Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single-writer/Multi-reader Classes: Object Transaction and Read Only Database Pool #327

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions fmdb.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
objects = {

/* Begin PBXBuildFile section */
25BBB47A1A43C422003CA4CE /* FMReadOnlyDatabasePool.h in Headers */ = {isa = PBXBuildFile; fileRef = 25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */; };
25BBB47B1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */; };
25BBB47C1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */; };
25BBB47D1A43C422003CA4CE /* FMDatabaseTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */; };
25BBB47E1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */; };
25BBB47F1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */; };
3354379C19E71096005661F3 /* FMResultSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3354379B19E71096005661F3 /* FMResultSetTests.m */; };
621721B21892BFE30006691F /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */; };
621721B31892BFE30006691F /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EC00A13E34D00A6D3E3 /* FMResultSet.m */; };
Expand Down Expand Up @@ -88,6 +94,10 @@

/* Begin PBXFileReference section */
08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMReadOnlyDatabasePool.h; path = src/fmdb/FMReadOnlyDatabasePool.h; sourceTree = "<group>"; };
25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMReadOnlyDatabasePool.m; path = src/fmdb/FMReadOnlyDatabasePool.m; sourceTree = "<group>"; };
25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabaseTransaction.h; path = src/fmdb/FMDatabaseTransaction.h; sourceTree = "<group>"; };
25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabaseTransaction.m; path = src/fmdb/FMDatabaseTransaction.m; sourceTree = "<group>"; };
32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fmdb_Prefix.pch; path = src/sample/fmdb_Prefix.pch; sourceTree = SOURCE_ROOT; };
3354379B19E71096005661F3 /* FMResultSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMResultSetTests.m; sourceTree = "<group>"; };
6290CBB5188FE836009790F8 /* libFMDB-IOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libFMDB-IOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -241,6 +251,10 @@
8314AF3018CD737D00EC0E25 /* fmdb */ = {
isa = PBXGroup;
children = (
25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */,
25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */,
25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */,
25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */,
8314AF3218CD73D600EC0E25 /* FMDB.h */,
CCC24EBA0A13E34D00A6D3E3 /* FMDatabase.h */,
CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */,
Expand Down Expand Up @@ -343,6 +357,8 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
25BBB47D1A43C422003CA4CE /* FMDatabaseTransaction.h in Headers */,
25BBB47A1A43C422003CA4CE /* FMReadOnlyDatabasePool.h in Headers */,
EE42910712B42FC90088BD94 /* FMDatabase.h in Headers */,
EE42910612B42FC30088BD94 /* FMDatabaseAdditions.h in Headers */,
EE42910912B42FD00088BD94 /* FMResultSet.h in Headers */,
Expand Down Expand Up @@ -478,6 +494,8 @@
buildActionMask = 2147483647;
files = (
621721B31892BFE30006691F /* FMResultSet.m in Sources */,
25BBB47F1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */,
25BBB47C1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */,
621721B21892BFE30006691F /* FMDatabase.m in Sources */,
621721B61892BFE30006691F /* FMDatabasePool.m in Sources */,
621721B41892BFE30006691F /* FMDatabaseQueue.m in Sources */,
Expand Down Expand Up @@ -523,6 +541,8 @@
buildActionMask = 2147483647;
files = (
EE42910812B42FCC0088BD94 /* FMDatabase.m in Sources */,
25BBB47E1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */,
25BBB47B1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */,
EE42910512B42FBC0088BD94 /* FMDatabaseAdditions.m in Sources */,
EE42910A12B42FD20088BD94 /* FMResultSet.m in Sources */,
CC9E4EBB13B31188005F9210 /* FMDatabasePool.m in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions src/fmdb/FMDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
#import "FMDatabaseAdditions.h"
#import "FMDatabaseQueue.h"
#import "FMDatabasePool.h"
#import "FMDatabaseTransaction.h"
#import "FMReadOnlyDatabasePool.h"
103 changes: 103 additions & 0 deletions src/fmdb/FMDatabaseTransaction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// FMDatabaseTransaction.h
// LayerKit
//
// Created by Blake Watters on 12/18/14.
// Copyright (c) 2014 Layer Inc. All rights reserved.
//

#import <FMDB/FMDB.h>

/**
@abstract The `FMDatabaseTransaction` class models a SQLite database transaction as an object. Write access is brokered via a semaphore.
@discussion The `FMDatabaseTransaction` object is an interface for mediating threaded access to a single writable database instance. The semaphore
guarantees that only a single consumer can access the database at a time without requiring the use of blocks to mediate access. Note that transaction
objects are single use. Attempting to reuse a transaction object after it has been committed or rolled back is programmer error and will result in a
runtime exception being raised.
*/
@interface FMDatabaseTransaction : NSObject

///--------------------------------------
/// @name Creating a Database Transaction
///--------------------------------------

/**
@abstract Creates and returns a new transaction for the given database. A shared semaphore with a count of 1 must be used to broker access
between all transaction objects.
@param database The writable database to create a transaction against.
@param semaphore A Grand Central Dispatch counting semaphore used to broker access to the database.
@return A newly created database transaction object.
*/
+ (instancetype)transactionWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore;

/**
@abstract Returns the database the transaction is bound to or `nil` if it has not yet been opened.
@discussion It is not safe to utilize the database reference until the transaction has been opened. The accessor will return `nil` until the transaction is opened.
*/
@property (nonatomic, readonly) FMDatabase *database;

///-----------------------------------
/// @name Inspecting Transaction State
///-----------------------------------

/**
@abstract Returns a Boolean value that indicates if the transaction has been opened.
@discussion The transaction is only considered opened if the underlying database is in a transaction and the receiver opened the transaction.
*/
@property (nonatomic, readonly) BOOL isOpen;

/**
@abstract Returns a Boolean value that indicates if the transaction is complete (from being committed or rolled back).
*/
@property (nonatomic, readonly) BOOL isComplete;

///------------------------------------------
/// @name Opening and Closing the Transaction
///------------------------------------------

/**
@abstract Opens the database transaction, gaining exclusive write access to the connection.
@param deferred A Boolean value that determine if an exclusive or a deferred transaction is opened.
@param error A pointer to an error object that is set upon failure to open the transaction.
@return A Boolean value that indicates if the transaction was successfully opened.
*/
- (BOOL)open:(BOOL)deferred error:(NSError **)error;

/**
@abstract Commits an open transaction.
@param error A pointer to an error object that is set upon failure to commit the transaction.
@return A Boolean value that indicates if the transaction was committed successfully.
*/
- (BOOL)commit:(NSError **)error;

/**
@abstract Rolls back an open transaction.
@param error A pointer to an error object that is set upon failure to roll back the transaction.
@return A Boolean value that indicates if the transaction was rolled back successfully.
*/
- (BOOL)rollback:(NSError **)error;

///------------------------------------------
/// @name Executing a Transaction via a Block
///------------------------------------------

/**
@abstract Executes the block in between a "transaction begin" and "transaction commit" statements.
@param database Database reference.
@param transactionBlock Transaction block with a `LYRDatabase` instance and a pointer to the `shouldRollback` switch.
*/
- (BOOL)performTransactionWithBlock:(void (^)(FMDatabase *database, BOOL *shouldRollback))transactionBlock;

///-------------------------------------
/// @name Configuring a Completion Block
///-------------------------------------

/**
@abstract Sets a completion block that gets executed after the transaction has been commited, rolled back or aborted.
@discussion If set, the completion block is guaranteed to be executed. If the transaction is aborted by falling out of scope then the completion block is invoked during `dealloc`.
If the transaction is aborted, then the completion block is called with a `isCommitted` value of `NO` and a `nil` error.
@param completion A block to executed upon completion of the transaction.
*/
- (void)setCompletionBlock:(void (^)(BOOL isCommitted, NSError *error))completionBlock;

@end
198 changes: 198 additions & 0 deletions src/fmdb/FMDatabaseTransaction.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//
// FMDatabaseTransaction.m
// LayerKit
//
// Created by Blake Watters on 12/18/14.
// Copyright (c) 2014 Layer Inc. All rights reserved.
//

#import "FMDatabaseTransaction.h"

@interface FMDatabaseTransaction ()
@property (nonatomic) FMDatabase *database;
@property (nonatomic) dispatch_semaphore_t semaphore;
@property (nonatomic) NSRecursiveLock *lock;
@property (nonatomic, copy) void (^completionBlock)(BOOL isCommitted, NSError *error);
@property (nonatomic) BOOL openedTransaction;
@end

@implementation FMDatabaseTransaction

+ (instancetype)transactionWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore
{
return [[self alloc] initWithDatabase:database semaphore:semaphore];
}

- (id)initWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore
{
NSParameterAssert(database);
NSParameterAssert(semaphore);
self = [super init];
if (self) {
_database = database;
_semaphore = semaphore;
_lock = [NSRecursiveLock new];
_lock.name = [NSString stringWithFormat:@"LYRDatabaseTransaction Lock %p", self];
_openedTransaction = NO;
}
return self;
}

- (id)init
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to call designated initializer." userInfo:nil];
}

- (void)dealloc
{
// Only signal and dispatch completion if we opened the transaction
if (_openedTransaction) {
if (_semaphore) {
if (self.isOpen) {
[self rollback:nil];
} else {
dispatch_semaphore_signal(_semaphore);
[self signalCompletionWithSuccess:NO error:nil];
}
} else {
[self signalCompletionWithSuccess:NO error:nil];
}
}
}

- (BOOL)performTransactionWithBlock:(void (^)(FMDatabase *database, BOOL *shouldRollback))transactionBlock
{
if (!transactionBlock) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to perform a transaction with a nil block." userInfo:nil];
}

// Begin transaction
BOOL success = [self open:YES error:nil];
if (!success) {
return NO;
}

// Execute transaction block
BOOL shouldRollback = NO;
transactionBlock(_database, &shouldRollback);

// Check if rollback is needed
if (shouldRollback) {
success = [self rollback:nil];
} else {
success = [self commit:nil];
}
return success;
}

- (BOOL)open:(BOOL)deferred error:(NSError **)error
{
if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction objects cannot be reused." userInfo:nil];
if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction objects cannot be reused." userInfo:nil];

dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
[self.lock lock];
if (self.database.inTransaction) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot open transaction because it has already been opened." userInfo:nil];
BOOL success = deferred ? [_database beginDeferredTransaction] : [_database beginTransaction];
if (success) {
self.openedTransaction = YES;
} else {
if (error) {
*error = [self.database lastError];
}
dispatch_semaphore_signal(_semaphore);
}
[self.lock unlock];
return success;
}

- (FMDatabase *)database
{
if (self.isOpen) {
return _database;
} else {
return nil;
}
}

- (BOOL)isOpen
{
[self.lock lock];
BOOL isOpen = self.openedTransaction && _database.inTransaction;
[self.lock unlock];
return isOpen;
}

- (void)releaseDatabase
{
[_database closeOpenResultSets];
_database = nil;
_isComplete = YES;
dispatch_semaphore_signal(_semaphore);
_semaphore = nil;
}

- (BOOL)commit:(NSError **)error
{
if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction has already been committed/rolled back." userInfo:nil];
if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction has already been committed/rolled back." userInfo:nil];

[self.lock lock];
if (!self.isOpen) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot commit transaction because it is not open." userInfo:nil];
NSError *outError = nil;
BOOL success = [_database commit];
if (!success) {
outError = [_database lastError];
}
[self releaseDatabase];
[self.lock unlock];
if (success) {
[self signalCompletionWithSuccess:YES error:nil];
} else {
[self signalCompletionWithSuccess:YES error:outError];
}
if (error) {
*error = outError;
}
return success;
}

- (BOOL)rollback:(NSError **)error
{
if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction has already been committed/rolled back." userInfo:nil];
if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction has already been committed/rolled back." userInfo:nil];

[self.lock lock];
if (!self.isOpen) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot rollback transaction because it is not open." userInfo:nil];
NSError *outError = nil;
BOOL success = [_database rollback];
if (!success) {
outError = [_database lastError];
}
[self releaseDatabase];
[self.lock unlock];
if (success) {
[self signalCompletionWithSuccess:NO error:nil];
} else {
[self signalCompletionWithSuccess:NO error:outError];
}
if (error) {
*error = outError;
}
return success;
}

- (void)setCompletionBlock:(void (^)(BOOL isCommitted, NSError *error))completionBlock
{
_completionBlock = [completionBlock copy];
}

- (void)signalCompletionWithSuccess:(BOOL)success error:(NSError *)error
{
if (self.completionBlock) {
self.completionBlock(success, error);
self.completionBlock = nil;
}
}

@end
Loading