@ -17,7 +17,12 @@ open class Storage {
public static let queuePrefix : String = " SessionDatabase "
private static let dbFileName : String = " Session.sqlite "
private static let kSQLCipherKeySpecLength : Int = 48
private static let writeWarningThreadshold : TimeInterval = 3
// / 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 a w a r n i n g w i l l b e l o g g e d b u t t h e t r a n s a c t i o n w i l l c o n t i n u e t o r u n
private static let slowTransactionThreshold : TimeInterval = 3
// / 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
private static var sharedDatabaseDirectoryPath : String { " \( FileManager . default . appSharedDataDirectoryPath ) /database " }
private static var databasePath : String { " \( Storage . sharedDatabaseDirectoryPath ) / \( Storage . dbFileName ) " }
@ -38,11 +43,7 @@ open class Storage {
public static let shared : Storage = Storage ( )
public private ( set ) var isValid : Bool = false
// / T h i s p r o p e r t y g e t s s e t w h e n t r i g g e r i n g t h e s u s p e n d / r e s u m e n o t i f i c a t i o n s f o r t h e d a t a b a s e b u t ` G R D B ` w i l l a t t e m p t t o
// / r e s u m e t h e s u s p e n t i o n w h e n i t a t t e m p t s t o p e r f o r m a w r i t e s o i t ' s p o s s i b l e f o r t h i s t o r e t u r n a * * f a l s e - p o s i t i v e * * s o
// / t h i s s h o u l d b e t a k e n i n t o c o n s i d e r a t i o n w h e n u s e d
public private ( set ) var isSuspendedUnsafe : Bool = false
public private ( set ) var isSuspended : Bool = false
// / T h i s p r o p e r t y g e t s s e t t h e f i r s t t i m e w e s u c c e s s f u l l y r e a d f r o m t h e d a t a b a s e
public private ( set ) var hasSuccessfullyRead : Bool = false
@ -98,8 +99,15 @@ open class Storage {
// C o n f i g u r e t h e d a t a b a s e a n d c r e a t e t h e D a t a b a s e P o o l f o r i n t e r a c t i n g w i t h t h e d a t a b a s e
var config = Configuration ( )
config . label = Storage . queuePrefix
config . maximumReaderCount = 10 // I n c r e a s e t h e m a x r e a d c o n n e c t i o n l i m i t - D e f a u l t i s 5
config . observesSuspensionNotifications = true // M i n i m i s e ` 0 x D E A D 1 0 C C ` e x c e p t i o n s
config . maximumReaderCount = 10 // / I n c r e a s e t h e m a x r e a d c o n n e c t i o n l i m i t - D e f a u l t i s 5
// / I t s e e m s w e s h o u l d d o t h i s p e r 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 / p u l l / 1 4 8 5 b u t w i t h t h i s c h a n g e
// / w e t h e n n e e d t o d e f i n e h o w l o n g a w r i t e t r a n s a c t i o n s h o u l d w a i t f o r b e f o r e t i m i n g o u t ( r e a d t r a n s a c t i o n s a l w a y s r u n
// / i n ` D E F E R R E D ` m o d e s o w o n ' t b e a f f e c t e d b y t h e s e s e t t i n g s )
config . defaultTransactionKind = . immediate
config . busyMode = . timeout ( Storage . writeTransactionStartTimeout )
// / L o a d i n t h e S Q L C i p h e r k e y s
config . prepareDatabase { db in
var keySpec : Data = try Storage . getOrGenerateDatabaseKeySpec ( )
defer { keySpec . resetBytes ( in : 0. . < keySpec . count ) } // R e s e t c o n t e n t i m m e d i a t e l y a f t e r u s e
@ -411,25 +419,30 @@ open class Storage {
// MARK: - F i l e M a n a g e m e n t
// / I n o r d e r t o a v o i d t h e ` 0 x d e a d 1 0 c c ` e x c e p t i o n w he n a c c e s s i n g t h e d a t a b a s e w h i l e a n o t h e r t a r g e t i s a c c e s s i n g i t w e c a l l
// / th e e x p e r i m e n t a l ` D a t a b a s e . s u s p e n d N o t i f i c a t i o n ` n o t i f i c a t i o n ( a n d s t o r e t h e c u r r e n t s u s p e n d e d s t a t e ) t o p r e v e n t
// / `G R D B ` f r o m t r y i n g t o a c c e s s t h e l o c k e d d a t a b a s e f i l e
// / I n o r d e r t o a v o i d t h e ` 0 x d e a d 1 0 c c ` e x c e p t i o n w e m a n u a l l y t r a c k w h e t h e r d a t a b a s e a c c e s s s h o u l d b e s u s p e n d e d , w h e n
// / in a s u s p e n d e d s t a t e t h i s c l a s s w i l l f a i l / r e j e c t a l l r e a d / w r i t e c a l l s m a d e t o i t . A d d i t i o n a l l y i f t h e r e w a s a n e x i s t i n g t r a n s a c t i o n
// / in p r o g r e s s i t w i l l b e i n t e r r u p t e d .
// /
// / T h e g e n e r a l l y s u g g e s t e d a p p r o a c h i s t o a v o i d t h i s e n t i r e l y b y n o t s t o r i n g t h e d a t a b a s e i n a n A p p G r o u p f o l d e r a n d s h a r i n g i t
// / w i t h e x t e n s i o n s - t h i s m a y b e p o s s i b l e b u t w i l l r e q u i r e s i g n i f i c a n t r e f a c t o r i n g a n d a p o t e n t i a l l y p a i n f u l m i g r a t i o n t o m o v e t h e
// / d a t a b a s e a n d o t h e r f i l e s i n t o t h e A p p f o l d e r
public static func suspendDatabaseAccess ( using dependencies : Dependencies = Dependencies ( ) ) {
Log . info ( " [Storage] suspendDatabaseAccess called. " )
NotificationCenter . default . post ( name : Database . suspendNotification , object : self )
if Storage . hasCreatedValidInstance { dependencies . storage . isSuspendedUnsafe = true }
public static func suspendDatabaseAccess ( using dependencies : Dependencies ) {
guard ! dependencies . storage . isSuspended else { return }
dependencies . storage . isSuspended = true
Log . info ( " [Storage] Database access suspended. " )
// / I n t e r r u p t a n y o p e n t r a n s a c t i o n s ( i f t h i s f u n c t i o n i s c a l l e d t h e n w e a r e e x p e c t i n g t h a t a l l p r o c e s s e s h a v e f i n i s h e d r u n n i n g
// / a n d d o n ' t a c t u a l l y w a n t a n y m o r e t r a n s a c t i o n s t o o c c u r )
dependencies . storage . dbWriter ? . interrupt ( )
}
// / T h i s m e t h o d r e v e r s e s t h e d a t a b a s e s u s p e n s i o n u s e d t o p r e v e n t t h e ` 0 x d e a d 1 0 c c ` e x c e p t i o n ( s e e ` s u s p e n d D a t a b a s e A c c e s s ( ) `
// / a b o v e f o r m o r e i n f o r m a t i o n
public static func resumeDatabaseAccess ( using dependencies : Dependencies = Dependencies ( ) ) {
NotificationCenter . default . post ( name : Database . resumeNotification , object : self )
if Storage . hasCreatedValidInstance { dependencies . storage . isSuspended Unsafe = false }
Log . info ( " [Storage] resumeDatabaseAccess call ed." )
public static func resumeDatabaseAccess ( using dependencies : Dependencies ) {
guard dependencies . storage . isSuspended else { return }
dependencies . storage . isSuspended = false
Log . info ( " [Storage] Database access resum ed." )
}
public static func resetAllStorage ( ) {
@ -466,78 +479,65 @@ open class Storage {
// MARK: - L o g g i n g F u n c t i o n s
private enum Action {
case read
case write
case logIfSlow
enum StorageState {
case valid ( DatabaseWriter )
case invalid ( Error )
init ( _ storage : Storage ) {
switch ( storage . isSuspended , storage . isValid , storage . dbWriter ) {
case ( true , _ , _ ) : self = . invalid ( StorageError . databaseSuspended )
case ( false , true , . some ( let dbWriter ) ) : self = . valid ( dbWriter )
default : self = . invalid ( StorageError . databaseInvalid )
}
}
static func logIfNeeded ( _ error : Error , isWrite : Bool ) {
switch error {
case DatabaseError . SQLITE_ABORT , DatabaseError . SQLITE_INTERRUPT :
let message : String = ( ( error as ? DatabaseError ) ? . message ? ? " Unknown " )
Log . error ( " [Storage] Database \( isWrite ? " write " : " read " ) failed due to error: \( message ) " )
case StorageError . databaseSuspended :
Log . error ( " [Storage] Database \( isWrite ? " write " : " read " ) failed as the database is suspended. " )
default : break
}
}
static func logIfNeeded < T > ( _ error : Error , isWrite : Bool ) -> T ? {
logIfNeeded ( error , isWrite : isWrite )
return nil
}
static func logIfNeeded < T > ( _ error : Error , isWrite : Bool ) -> AnyPublisher < T , Error > {
logIfNeeded ( error , isWrite : isWrite )
return Fail < T , Error > ( error : error ) . eraseToAnyPublisher ( )
}
}
private typealias CallInfo = ( storage : Storage ? , actions : [ Action ] , file : String , function : String , line : Int )
private static func perform < T > (
info : CallInfo ,
updates : @ escaping ( Database ) throws -> T
) -> ( Database ) throws -> T {
return { db in
let start : CFTimeInterval = CACurrentMediaTime ( )
let actionName : String = ( info . actions . contains ( . write ) ? " write " : " read " )
let fileName : String = ( info . file . components ( separatedBy : " / " ) . last . map { " \( $0 ) : \( info . line ) " } ? ? " " )
let timeout : Timer ? = {
guard info . actions . contains ( . logIfSlow ) else { return nil }
return Timer . scheduledTimerOnMainThread ( withTimeInterval : Storage . writeWarningThreadshold ) {
$0 . invalidate ( )
// D o n ' t w a n t t o l o g o n t h e m a i n t h r e a d a s t o a v o i d c o n f u s i o n w h e n d e b u g g i n g i s s u e s
DispatchQueue . global ( qos : . background ) . async {
Log . warn ( " [Storage \( fileName ) ] Slow \( actionName ) taking longer than \( Storage . writeWarningThreadshold , format : " .2 " , omitZeroDecimal : true ) s - \( info . function ) " )
}
}
} ( )
guard info . storage ? . isSuspended = = false else { throw StorageError . databaseSuspended }
// I f w e t i m e d o u t a n d a r e l o g g i n g s l o w a c t i o n s t h e n l o g t h e a c t u a l d u r a t i o n t o h e l p u s
// p r i o r i t i s e p e r f o r m a n c e i s s u e s
defer {
if timeout != nil && timeout ? . isValid = = false {
let end : CFTimeInterval = CACurrentMediaTime ( )
DispatchQueue . global ( qos : . background ) . async {
Log . warn ( " [Storage \( fileName ) ] Slow \( actionName ) completed after \( end - start , format : " .2 " , omitZeroDecimal : true ) s " )
}
}
timeout ? . invalidate ( )
}
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 . actions {
case [ . write ] , [ . write , . logIfSlow ] : info . storage ? . hasSuccessfullyWritten = true
case [ . read ] , [ . read , . logIfSlow ] : info . storage ? . hasSuccessfullyRead = true
default : break
switch info . isWrite {
case true : info . storage ? . hasSuccessfullyWritten = true
case false : info . storage ? . hasSuccessfullyRead = true
}
return result
}
}
private static func logIfNeeded ( _ error : Error , isWrite : Bool ) {
switch error {
case DatabaseError . SQLITE_ABORT :
let message : String = ( ( error as ? DatabaseError ) ? . message ? ? " Unknown " )
SNLog ( " [Storage] Database \( isWrite ? " write " : " read " ) failed due to error: \( message ) " )
default : break
}
}
private static func logIfNeeded < T > ( _ error : Error , isWrite : Bool ) -> T ? {
logIfNeeded ( error , isWrite : isWrite )
return nil
}
// MARK: - F u n c t i o n s
@ discardableResult public func write < T > (
@ -547,28 +547,13 @@ open class Storage {
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T ?
) -> T ? {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return nil }
let info : CallInfo = { [ weak self ] in ( self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
do { return try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) }
catch { return Storage . logIfNeeded ( error , isWrite : true ) }
}
open func writeAsync < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T
) {
writeAsync (
fileName : fileName ,
functionName : functionName ,
lineNumber : lineNumber ,
using : dependencies ,
updates : updates ,
completion : { _ , _ 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 )
do { return try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) }
catch { return StorageState . logIfNeeded ( error , isWrite : true ) }
}
}
open func writeAsync < T > (
@ -577,23 +562,24 @@ open class Storage {
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T ,
completion : @ escaping ( Database , Swift . Result < T , Error > ) throws -> Void
completion : @ escaping ( Database , Swift . Result < T , Error > ) throws -> Void = { _ , _ in }
) {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return }
let info : CallInfo = { [ weak self ] in ( self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
dbWriter . asyncWrite (
Storage . perform ( info : info , updates : updates ) ,
completion : { db , result in
switch result {
case . failure ( let error ) : Storage . logIfNeeded ( error , isWrite : true )
default : break
}
try ? completion ( db , result )
}
)
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 )
}
)
}
}
open func writePublisher < T > (
@ -603,75 +589,73 @@ open class Storage {
using dependencies : Dependencies = Dependencies ( ) ,
updates : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else {
return Fail < T , Error > ( error : StorageError . databaseInvalid )
. eraseToAnyPublisher ( )
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : true )
case . valid ( let dbWriter ) :
// / * * 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 { resolver in
do { resolver ( Result . success ( try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) ) ) }
catch {
StorageState . logIfNeeded ( error , isWrite : true )
resolver ( Result . failure ( error ) )
}
}
} . eraseToAnyPublisher ( )
}
let info : CallInfo = { [ weak self ] in ( self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
// / * * 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
return Deferred {
Future { resolver in
do { resolver ( Result . success ( try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) ) ) }
catch {
Storage . logIfNeeded ( error , isWrite : true )
resolver ( Result . failure ( error ) )
}
}
} . eraseToAnyPublisher ( )
}
open func readPublisher < T > (
@ discardableResult public func read < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
value : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else {
return Fail < T , Error > ( error : StorageError . databaseInvalid )
. eraseToAnyPublisher ( )
_ 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 ) }
}
let info : CallInfo = { [ weak self ] in ( self , [ . read ] , fileName , functionName , lineNumber ) } ( )
// / * * 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
return Deferred {
Future { resolver in
do { resolver ( Result . success ( try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) ) ) }
catch {
Storage . logIfNeeded ( error , isWrite : false )
resolver ( Result . failure ( error ) )
}
}
} . eraseToAnyPublisher ( )
}
@ discardableResult public func read < T > (
open func readPublisher < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
_ value : @ escaping ( Database ) throws -> T ?
) -> T ? {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return nil }
let info : CallInfo = { [ weak self ] in ( self , [ . read ] , fileName , functionName , lineNumber ) } ( )
do { return try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) }
catch { return Storage . logIfNeeded ( error , isWrite : false ) }
value : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
switch StorageState ( self ) {
case . invalid ( let error ) : return StorageState . logIfNeeded ( error , isWrite : false )
case . valid ( let dbWriter ) :
// / * * 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 { resolver in
do { resolver ( Result . success ( try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) ) ) }
catch {
StorageState . logIfNeeded ( error , isWrite : false )
resolver ( Result . failure ( error ) )
}
}
} . eraseToAnyPublisher ( )
}
}
// / 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
@ -779,3 +763,79 @@ public extension Storage {
}
}
#endif
// MARK: - C a l l I n f o
private extension Storage {
class CallInfo {
let file : String
let function : String
let line : Int
let isWrite : Bool
weak var storage : Storage ?
var callInfo : String {
let fileInfo : String = ( file . components ( separatedBy : " / " ) . last . map { " \( $0 ) : \( line ) - " } ? ? " " )
return " \( fileInfo ) \( function ) "
}
init (
_ file : String ,
_ function : String ,
_ line : Int ,
_ isWrite : Bool ,
_ storage : Storage ?
) {
self . file = file
self . function = function
self . line = line
self . isWrite = isWrite
self . storage = storage
}
}
}
// MARK: - T r a n s a c t i o n T i m e r
private extension Storage {
private static let timerQueue = DispatchQueue ( label : " \( Storage . queuePrefix ) -.transactionTimer " , qos : . background )
class TransactionTimer {
private let info : Storage . CallInfo
private let start : CFTimeInterval = CACurrentMediaTime ( )
private var timer : DispatchSourceTimer ? = DispatchSource . makeTimerSource ( queue : Storage . timerQueue )
private var wasSlowTransaction : Bool = false
private init ( info : Storage . CallInfo ) {
self . info = info
}
static func start ( duration : TimeInterval , info : Storage . CallInfo ) -> TransactionTimer {
let result : TransactionTimer = TransactionTimer ( info : info )
result . timer ? . schedule ( deadline : . now ( ) + . seconds ( Int ( duration ) ) , repeating : . infinity ) // I n f i n i t y t o f i r e o n c e
result . timer ? . setEventHandler { [ weak result ] in
result ? . timer ? . cancel ( )
result ? . timer = nil
let action : String = ( info . isWrite ? " write " : " read " )
Log . warn ( " [Storage] Slow \( action ) taking longer than \( Storage . slowTransactionThreshold , format : " .2 " , omitZeroDecimal : true ) s - [ \( info . callInfo ) ] " )
result ? . wasSlowTransaction = true
}
result . timer ? . resume ( )
return result
}
func stop ( ) {
timer ? . cancel ( )
timer = nil
guard wasSlowTransaction else { return }
let end : CFTimeInterval = CACurrentMediaTime ( )
let action : String = ( info . isWrite ? " write " : " read " )
Log . warn ( " [Storage] Slow \( action ) completed after \( end - start , format : " .2 " , omitZeroDecimal : true ) s - [ \( info . callInfo ) ] " )
}
}
}