@ -29,6 +29,9 @@ open class Storage {
// / W h e n a t t e m p t i n g t o d o a w r i t e t h e t r a n s a c t i o n w i l l w a i t t h i s l o n g t o a c q u i t e a l o c k b e f o r e f a i l i n g
private static let writeTransactionStartTimeout : TimeInterval = 5
// / I f a t r a n s a c t i o n t a k e s l o n g e r t h a n t h i s d u r a t i o n t h e n w e s h o u l d f a i l t h e t r a n s a c t i o n r a t h e r t h a n k e e p h a n g i n g
private static let transactionDeadlockTimeoutSeconds : Int = 5
private static var sharedDatabaseDirectoryPath : String { " \( FileManager . default . appSharedDataDirectoryPath ) /database " }
private static var databasePath : String { " \( Storage . sharedDatabaseDirectoryPath ) / \( Storage . dbFileName ) " }
private static var databasePathShm : String { " \( Storage . sharedDatabaseDirectoryPath ) / \( Storage . dbFileName ) -shm " }
@ -322,19 +325,14 @@ open class Storage {
guard async else { return migrationCompleted ( Result ( try migrator . migrate ( dbWriter ) ) ) }
migrator . asyncMigrate ( dbWriter ) { result in
let finalResult : Swift. Result< Void , Error > = {
let finalResult : Result< Void , Error > = {
switch result {
case . failure ( let error ) : return . failure ( error )
case . success : return . success ( ( ) )
}
} ( )
// N o t e : W e n e e d t o d i s p a t c h t h i s a f t e r a s m a l l 0 . 0 1 d e l a y t o p r e v e n t a n y p o t e n t i a l
// r e - e n t r a n c y i s s u e s s i n c e t h e ' a s y n c M i g r a t e ' r e t u r n s a r e s u l t c o n t a i n i n g a D B i n s t a n c e
// w i t h i n a t r a n s a c t i o n
DispatchQueue . global ( qos : . userInitiated ) . asyncAfter ( deadline : . now ( ) + 0.01 , using : dependencies ) {
migrationCompleted ( finalResult )
}
migrationCompleted ( finalResult )
}
}
@ -542,6 +540,9 @@ open class Storage {
case StorageError . databaseSuspended :
Log . error ( " [Storage] Database \( isWrite ? " write " : " read " ) failed as the database is suspended. " )
case StorageError . transactionDeadlockTimeout :
Log . critical ( " [Storage] Database \( isWrite ? " write " : " read " ) failed due to a potential synchronous query deadlock timeout. " )
default : break
}
}
@ -557,71 +558,150 @@ open class Storage {
}
}
private static func perform < T > (
info : CallInfo ,
updates : @ escaping ( Database ) throws -> T
) -> ( Database ) throws -> T {
return { db in
guard info . storage ? . isSuspended = = false else { throw StorageError . databaseSuspended }
let timer : TransactionTimer = TransactionTimer . start ( duration : Storage . slowTransactionThreshold , info : info )
defer { timer . stop ( ) }
// G e t t h e r e s u l t
let result : T = try updates ( db )
// U p d a t e t h e s t a t e f l a g s
switch info . isWrite {
case true : info . storage ? . hasSuccessfullyWritten = true
case false : info . storage ? . hasSuccessfullyRead = true
}
return result
// MARK: - O p e r a t i o n s
private static func track < T > (
_ db : Database ,
_ info : CallInfo ,
_ operation : @ escaping ( Database ) throws -> T
) throws -> T {
guard info . storage ? . isSuspended = = false else { throw StorageError . databaseSuspended }
let timer : TransactionTimer = TransactionTimer . start (
duration : Storage . slowTransactionThreshold ,
info : info
)
defer { timer . stop ( ) }
// G e t t h e r e s u l t
let result : T = try operation ( db )
// U p d a t e t h e s t a t e f l a g s
switch info . isWrite {
case true : info . storage ? . hasSuccessfullyWritten = true
case false : info . storage ? . hasSuccessfullyRead = true
}
return result
}
// / T h i s f u n c t i o n m a n u a l l y p e r f o r m s ` r e a d ` / ` w r i t e ` o p e r a t i o n s i n e i t h e r a s y n c h r o n o u s o r a s y n c r o n o u s w a y u s i n g a s e m a p h o r e t o
// / b l o c k t h e s y n c r h o n o u s v e r s i o n b e c a u s e ` G R D B ` h a s a n i n t e r n a l a s s e r t i o n w h e n u s i n g i t ' s b u i l t - i n s y n c h r o n o u s ` r e a d ` / ` w r i t e `
// / f u n c t i o n s t o p r e v e n t r e e n t r a n c y w h i c h i s u n s u p p o r t e d
// /
// / U n f o r t u n a t e l y t h i s r e s u l t s i n t h e c o d e g e t t i n g m e s s y w h e n t r y i n g t o c h a i n m u l t i p l e d a t a b a s e t r a n s a c t i o n s ( e v e n
// / w h e n u s i n g ` d b . a f t e r N e x t T r a n s a c t i o n ` ) w h i c h i s s o m e w h a t u n i n t u i t i v e
// /
// / T h e ` a s y n c ` v a r i a n t s d o n ' t n e e d t o w o r r y a b o u t t h i s r e e n t r a n c y i s s u e s o i n s t e a d w e r o u t e w e u s e t h o s e f o r a l l o p e r a t i o n s i n s t e a d
// / a n d j u s t b l o c k t h e t h r e a d w h e n w e w a n t t o p e r f o r m a s y n c h r o n o u s o p e r a t i o n
@ discardableResult private static func performOperation < T > (
_ info : CallInfo ,
_ operation : @ escaping ( Database ) throws -> T ,
_ completion : ( ( Result < T , Error > ) -> Void ) ? = nil
) -> Result < T , Error > {
let semaphore : DispatchSemaphore ? = ( info . isAsync ? nil : DispatchSemaphore ( value : 0 ) )
var result : Result < T , Error > = . failure ( StorageError . invalidQueryResult )
// / P e r f o r m t h e a c t u a l o p e r a t i o n
switch ( StorageState ( info . storage ) , info . isWrite ) {
case ( . invalid ( let error ) , _ ) : result = . failure ( error )
case ( . valid ( let dbWriter ) , true ) :
dbWriter . asyncWrite (
{ db in result = . success ( try Storage . track ( db , info , operation ) ) } ,
completion : { _ , dbResult in
switch dbResult {
case . success : break
case . failure ( let error ) : result = . failure ( error )
}
semaphore ? . signal ( )
completion ? ( result )
}
)
case ( . valid ( let dbWriter ) , false ) :
dbWriter . asyncRead { dbResult in
do {
switch dbResult {
case . failure ( let error ) : throw error
case . success ( let db ) : result = . success ( try Storage . track ( db , info , operation ) )
}
} catch {
result = . failure ( error )
}
semaphore ? . signal ( )
completion ? ( result )
}
}
// / I f t h i s i s a s y n c h r o n o u s o p e r a t i o n t h e n ` s e m a p h o r e ` w i l l e x i s t a n d w i l l b l o c k h e r e w a i t i n g o n t h e s i g n a l f r o m o n e o f t h e
// / a b o v e c l o s u r e s t o b e s e n t
let semaphoreResult : DispatchTimeoutResult ? = semaphore ? . wait ( timeout : . now ( ) + . seconds ( Storage . transactionDeadlockTimeoutSeconds ) )
// / I f t h e t r a n s a c t i o n t i m e d o u t t h e n l o g t h e e r r o r a n d r e p o r t a f a i l u r e
guard semaphoreResult != . timedOut else {
StorageState . logIfNeeded ( StorageError . transactionDeadlockTimeout , isWrite : info . isWrite )
return . failure ( StorageError . transactionDeadlockTimeout )
}
// / L o g t h e e r r o r i f n e e d e d
switch result {
case . success : break
case . failure ( let error ) : StorageState . logIfNeeded ( error , isWrite : info . isWrite )
}
return result
}
private func performPublisherOperation < T > (
_ fileName : String ,
_ functionName : String ,
_ lineNumber : Int ,
isWrite : Bool ,
_ operation : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : false )
case . valid :
// / * * N o t e : * * G R D B d o e s h a v e ` r e a d P u b l i s h e r ` / ` w r i t e P u b l i s h e r ` f u n c t i o n s b u t i t a p p e a r s t o a s y n c h r o n o u s l y
// / t r i g g e r b o t h t h e ` o u t p u t ` a n d ` c o m p l e t e ` c l o s u r e s a t t h e s a m e t i m e w h i c h c a u s e s a l o t o f u n e x p e c t e d
// / b e h a v i o u r s ( t h i s b e h a v i o u r i s a p p a r e n t l y e x p e c t e d b u t s t i l l c a u s e s a n u m b e r o f o d d b e h a v i o u r s i n o u r c o d e
// / f o r m o r e i n f o r m a t i o n s e e h t t p s : / / g i t h u b . c o m / g r o u e / G R D B . s w i f t / i s s u e s / 1 3 3 4 )
// /
// / I n s t e a d o f t h i s w e a r e j u s t u s i n g ` D e f e r r e d { F u t u r e { } } ` w h i c h i s e x e c u t e d o n t h e s p e c i f i e d s c h e d u l e d
// / w h i c h b e h a v e s i n a m u c h m o r e e x p e c t e d w a y t h a n t h e G R D B ` r e a d P u b l i s h e r ` / ` w r i t e P u b l i s h e r ` d o e s
let info : CallInfo = CallInfo ( self , fileName , functionName , lineNumber , . syncWrite )
return Deferred {
Future { resolver in
resolver ( Storage . performOperation ( info , operation ) )
}
} . eraseToAnyPublisher ( )
}
}
// MARK: - F u n c t i o n s
@ discardableResult public func write < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
fileName file : String = #file ,
functionName funcN : String = #function ,
lineNumber line : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T ?
) -> T ? {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : true )
case . valid ( let dbWriter ) :
let info : CallInfo = CallInfo ( fileName , functionName , lineNumber , true , self )
do { return try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) }
catch { return StorageState . logIfNeeded ( error , isWrite : true ) }
switch Storage . performOperation ( CallInfo ( self , file , funcN , line , . syncWrite ) , updates ) {
case . failure : return nil
case . success ( let result ) : return result
}
}
open func writeAsync < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
fileName file : String = #file ,
functionName funcN : String = #function ,
lineNumber line : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T ,
completion : @ escaping ( Database , Swift . Result < T , Error > ) throws -> Void = { _ , _ in }
completion : @ escaping ( Result< T , Error > ) -> Void = { _ in }
) {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : true )
case . valid ( let dbWriter ) :
let info : CallInfo = CallInfo ( fileName , functionName , lineNumber , true , self )
dbWriter . asyncWrite (
Storage . perform ( info : info , updates : updates ) ,
completion : { db , result in
switch result {
case . failure ( let error ) : StorageState . logIfNeeded ( error , isWrite : true )
default : break
}
try ? completion ( db , result )
}
)
}
Storage . performOperation ( CallInfo ( self , file , funcN , line , . asyncWrite ) , updates , completion )
}
open func writePublisher < T > (
@ -631,50 +711,19 @@ open class Storage {
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : true )
case . valid :
// / * * N o t e : * * G R D B d o e s h a v e a ` w r i t e P u b l i s h e r ` m e t h o d b u t i t a p p e a r s t o a s y n c h r o n o u s l y t r i g g e r
// / b o t h t h e ` o u t p u t ` a n d ` c o m p l e t e ` c l o s u r e s a t t h e s a m e t i m e w h i c h c a u s e s a l o t o f u n e x p e c t e d
// / b e h a v i o u r s ( t h i s b e h a v i o u r i s a p p a r e n t l y e x p e c t e d b u t s t i l l c a u s e s a n u m b e r o f o d d b e h a v i o u r s i n o u r c o d e
// / f o r m o r e i n f o r m a t i o n s e e h t t p s : / / g i t h u b . c o m / g r o u e / G R D B . s w i f t / i s s u e s / 1 3 3 4 )
// /
// / I n s t e a d o f t h i s w e a r e j u s t u s i n g ` D e f e r r e d { F u t u r e { } } ` w h i c h i s e x e c u t e d o n t h e s p e c i f i e d s c h e d u l e d
// / w h i c h b e h a v e s i n a m u c h m o r e e x p e c t e d w a y t h a n t h e G R D B ` w r i t e P u b l i s h e r ` d o e s
let info : CallInfo = CallInfo ( fileName , functionName , lineNumber , true , self )
return Deferred {
Future { [ weak self ] resolver in
// / T h e ` S t o r a g e S t a t e ` m a y h a v e c h a n g e d b e t w e e n t h e c r e a t i o n o f t h e p u b l i s h e r a n d i t a c t u a l l y
// / b e i n g e x e c u t e d s o w e n e e d t o c h e c k a g a i n
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : true )
case . valid ( let dbWriter ) :
do {
resolver ( Result . success ( try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) ) )
}
catch {
StorageState . logIfNeeded ( error , isWrite : true )
resolver ( Result . failure ( error ) )
}
}
}
} . eraseToAnyPublisher ( )
}
return performPublisherOperation ( fileName , functionName , lineNumber , isWrite : true , updates )
}
@ discardableResult public func read < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
fileName file : String = #file ,
functionName funcN : String = #function ,
lineNumber line : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
_ value : @ escaping ( Database ) throws -> T ?
) -> T ? {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : false )
case . valid ( let dbWriter ) :
let info : CallInfo = CallInfo ( fileName , functionName , lineNumber , false , self )
do { return try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) }
catch { return StorageState . logIfNeeded ( error , isWrite : false ) }
switch Storage . performOperation ( CallInfo ( self , file , funcN , line , . syncRead ) , value ) {
case . failure : return nil
case . success ( let result ) : return result
}
}
@ -685,35 +734,7 @@ open class Storage {
using dependencies : Dependencies = Dependencies ( ) ,
value : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : false )
case . valid :
// / * * N o t e : * * G R D B d o e s h a v e a ` r e a d P u b l i s h e r ` m e t h o d b u t i t a p p e a r s t o a s y n c h r o n o u s l y t r i g g e r
// / b o t h t h e ` o u t p u t ` a n d ` c o m p l e t e ` c l o s u r e s a t t h e s a m e t i m e w h i c h c a u s e s a l o t o f u n e x p e c t e d
// / b e h a v i o u r s ( t h i s b e h a v i o u r i s a p p a r e n t l y e x p e c t e d b u t s t i l l c a u s e s a n u m b e r o f o d d b e h a v i o u r s i n o u r c o d e
// / f o r m o r e i n f o r m a t i o n s e e h t t p s : / / g i t h u b . c o m / g r o u e / G R D B . s w i f t / i s s u e s / 1 3 3 4 )
// /
// / I n s t e a d o f t h i s w e a r e j u s t u s i n g ` D e f e r r e d { F u t u r e { } } ` w h i c h i s e x e c u t e d o n t h e s p e c i f i e d s c h e d u l e d
// / w h i c h b e h a v e s i n a m u c h m o r e e x p e c t e d w a y t h a n t h e G R D B ` r e a d P u b l i s h e r ` d o e s
let info : CallInfo = CallInfo ( fileName , functionName , lineNumber , false , self )
return Deferred {
Future { [ weak self ] resolver in
// / T h e ` S t o r a g e S t a t e ` m a y h a v e c h a n g e d b e t w e e n t h e c r e a t i o n o f t h e p u b l i s h e r a n d i t a c t u a l l y
// / b e i n g e x e c u t e d s o w e n e e d t o c h e c k a g a i n
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : false )
case . valid ( let dbWriter ) :
do {
resolver ( Result . success ( try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) ) )
}
catch {
StorageState . logIfNeeded ( error , isWrite : false )
resolver ( Result . failure ( error ) )
}
}
}
} . eraseToAnyPublisher ( )
}
return performPublisherOperation ( fileName , functionName , lineNumber , isWrite : false , value )
}
// / R e v e r t o t h e ` V a l u e O b s e r v a t i o n . s t a r t ` m e t h o d f o r f u l l d o c u m e n t a t i o n
@ -904,11 +925,18 @@ public extension Storage {
private extension Storage {
class CallInfo {
enum Behaviour {
case syncRead
case asyncRead
case syncWrite
case asyncWrite
}
weak var storage : Storage ?
let file : String
let function : String
let line : Int
let isWrite : Bool
weak var storage : Storage ?
let behaviour : Behaviour
var callInfo : String {
let fileInfo : String = ( file . components ( separatedBy : " / " ) . last . map { " \( $0 ) : \( line ) - " } ? ? " " )
@ -916,18 +944,31 @@ private extension Storage {
return " \( fileInfo ) \( function ) "
}
var isWrite : Bool {
switch behaviour {
case . syncWrite , . asyncWrite : return true
case . syncRead , . asyncRead : return false
}
}
var isAsync : Bool {
switch behaviour {
case . asyncRead , . asyncWrite : return true
case . syncRead , . syncWrite : return false
}
}
init (
_ storage : Storage ? ,
_ file : String ,
_ function : String ,
_ line : Int ,
_ isWrite : Bool ,
_ storage : Storage ?
_ behaviour : Behaviour
) {
self . storage = storage
self . file = file
self . function = function
self . line = line
self . isWrite = isWrite
self . storage = storage
self . behaviour = behaviour
}
}
}