Skip to content

Commit 0a70a65

Browse files
committed
Exponential backoff
1 parent e8129be commit 0a70a65

File tree

1 file changed

+58
-17
lines changed

1 file changed

+58
-17
lines changed

Sources/PackageCollections/Storage/SQLitePackageCollectionsStorage.swift

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,28 +83,40 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
8383
// Signal long-running operation (e.g., populateTargetTrie) to stop
8484
self.isShuttingdown.put(true)
8585

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+
86111
try self.stateLock.withLock {
87112
if case .connected(let db) = self.state {
88113
do {
89114
try db.close()
90115
} 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 {
108120
throw StringError("Failed to close database")
109121
}
110122
}
@@ -113,6 +125,35 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
113125
}
114126
}
115127

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+
116157
func put(collection: Model.Collection,
117158
callback: @escaping (Result<Model.Collection, Error>) -> Void) {
118159
self.queue.async {

0 commit comments

Comments
 (0)