@@ -83,28 +83,40 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
83
83
// Signal long-running operation (e.g., populateTargetTrie) to stop
84
84
self . isShuttingdown. put ( true )
85
85
86
+ func retryClose( db: SQLite , exponentialBackoff: inout ExponentialBackoff ) throws {
87
+ let semaphore = DispatchSemaphore ( value: 0 )
88
+ let callback = { ( result: Result < Void , Error > ) in
89
+ // If it has failed, the semaphore will timeout in which case we will retry
90
+ if case . success = result {
91
+ semaphore. signal ( )
92
+ }
93
+ }
94
+
95
+ // This throws error if we have exhausted our attempts
96
+ let delay = try exponentialBackoff. nextDelay ( )
97
+ self . queue. asyncAfter ( deadline: . now( ) + delay) {
98
+ do {
99
+ try db. close ( )
100
+ callback ( . success( ( ) ) )
101
+ } catch {
102
+ callback ( . failure( error) )
103
+ }
104
+ }
105
+ // Add some buffer to allow `asyncAfter` to run
106
+ guard case . success = semaphore. wait ( timeout: . now( ) + delay + . milliseconds( 50 ) ) else {
107
+ return try retryClose ( db: db, exponentialBackoff: & exponentialBackoff)
108
+ }
109
+ }
110
+
86
111
try self . stateLock. withLock {
87
112
if case . connected( let db) = self . state {
88
113
do {
89
114
try db. close ( )
90
115
} catch {
91
- // This could be because another operation is still running. Wait a bit then try again.
92
- let semaphore = DispatchSemaphore ( value: 0 )
93
- let callback = { ( result: Result < Void , Error > ) in
94
- // If second attempt fails as well, semaphore will timeout in which case we will throw error.
95
- if case . success = result {
96
- semaphore. signal ( )
97
- }
98
- }
99
- self . queue. asyncAfter ( deadline: . now( ) + . milliseconds( 200 ) ) {
100
- do {
101
- try db. close ( )
102
- callback ( . success( ( ) ) )
103
- } catch {
104
- callback ( . failure( error) )
105
- }
106
- }
107
- guard case . success = semaphore. wait ( timeout: . now( ) + . milliseconds( 300 ) ) else {
116
+ var exponentialBackoff = ExponentialBackoff ( )
117
+ do {
118
+ try retryClose ( db: db, exponentialBackoff: & exponentialBackoff)
119
+ } catch {
108
120
throw StringError ( " Failed to close database " )
109
121
}
110
122
}
@@ -113,6 +125,35 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
113
125
}
114
126
}
115
127
128
+ private struct ExponentialBackoff {
129
+ let intervalInMilliseconds : Int
130
+ let randomizationFactor : Int
131
+ let maximumAttempts : Int
132
+
133
+ var attempts : Int = 0
134
+
135
+ var canRetry : Bool {
136
+ self . attempts < self . maximumAttempts
137
+ }
138
+
139
+ init ( intervalInMilliseconds: Int = 100 , randomizationFactor: Int = 100 , maximumAttempts: Int = 3 ) {
140
+ self . intervalInMilliseconds = intervalInMilliseconds
141
+ self . randomizationFactor = randomizationFactor
142
+ self . maximumAttempts = maximumAttempts
143
+ }
144
+
145
+ mutating func nextDelay( ) throws -> DispatchTimeInterval {
146
+ guard self . canRetry else {
147
+ print ( " exhausted: \( attempts) " )
148
+ throw StringError ( " Maximum attempts reached " )
149
+ }
150
+ let delay = Int ( pow ( 2.0 , Double ( self . attempts) ) ) * intervalInMilliseconds
151
+ let jitter = Int . random ( in: 0 ... self . randomizationFactor)
152
+ self . attempts += 1
153
+ return . milliseconds( delay + jitter)
154
+ }
155
+ }
156
+
116
157
func put( collection: Model . Collection ,
117
158
callback: @escaping ( Result < Model . Collection , Error > ) -> Void ) {
118
159
self . queue. async {
0 commit comments