File size: 4,246 Bytes
6ee917b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import Foundation
import AsyncHTTPClient
import NIOCore
import Logging


extension HTTPClientResponse {
    /// Throw if a transient error occurred. A retry could be successful
    func throwOnTransientError() throws {
        if isTransientError {
            throw CurlError.downloadFailed(code: status)
        }
    }
    
    /// True is status code contains an error that is retirable. E.g. Gateway timeout or too many requests
    var isTransientError: Bool {
        return [
            .requestTimeout,
            .tooManyRequests,
            .internalServerError,
            .badGateway,
            .serviceUnavailable,
            .gatewayTimeout
        ].contains(status)
    }

    func throwOnFatalError() throws {
        if status == .unauthorized {
            throw CurlErrorNonRetry.unauthorized
        }
    }
}

extension HTTPClient {
    /**
     Retry HTTP requests on error
     */
    func executeRetry(_ request: HTTPClientRequest,
                       logger: Logger,
                       deadline: Date = .minutes(60),
                       timeoutPerRequest: TimeAmount = .seconds(30),
                       backoffFactor: TimeAmount = .milliseconds(1000),
                       backoffMaximum: TimeAmount = .seconds(30),
                       error404WaitTime: TimeAmount? = nil) async throws -> HTTPClientResponse  {
        var lastPrint = Date(timeIntervalSince1970: 0)
        let startTime = Date()
        var n = 0
        while true {
            do {
                n += 1
                let response = try await execute(request, timeout: timeoutPerRequest, logger: logger)
                logger.debug("Response for HTTP request #\(n) returned HTTP status code: \(response.status), from URL \(request.url)")
                try response.throwOnTransientError()
                try response.throwOnFatalError()
                if error404WaitTime != nil && response.status == .notFound {
                    throw CurlError.fileNotFound
                }
                return response
            } catch CurlErrorNonRetry.unauthorized {
                logger.info("Download failed with 401 Unauthorized error, credentials rejected. Possibly outdated API key.")
                throw CurlErrorNonRetry.unauthorized
            } catch {
                var wait = TimeAmount.nanoseconds(min(backoffFactor.nanoseconds * Int64(pow(2, Double(n-1))), backoffMaximum.nanoseconds))
                
                if let ioerror = error as? IOError, [104,54].contains(ioerror.errnoCode), n <= 2 {
                    /// MeteoFrance API resets the connection very frequently causing large delays in downloading
                    /// Immediately retry twice
                    wait = .zero
                }
                if let error404WaitTime, case CurlError.fileNotFound = error {
                    wait = error404WaitTime
                }
                
                let timeElapsed = Date().timeIntervalSince(startTime)
                if Date().timeIntervalSince(lastPrint) > 60 {
                    logger.info("Download failed. Attempt \(n). Elapsed \(timeElapsed.prettyPrint). Retry in \(wait.prettyPrint). Error '\(error) [\(type(of: error))]'")
                    lastPrint = Date()
                }
                if Date() > deadline {
                    logger.error("Deadline reached. Attempt \(n). Elapsed \(timeElapsed.prettyPrint).  Error '\(error) [\(type(of: error))]'")
                    throw CurlError.timeoutReached
                }
                try await _Concurrency.Task.sleep(nanoseconds: UInt64(wait.nanoseconds))
            }
        }
    }
}

extension TimeAmount {
    var prettyPrint: String {
        return (Double(nanoseconds) / 1_000_000_000).asSecondsPrettyPrint
    }
}

extension TimeInterval {
    var prettyPrint: String {
        return self.asSecondsPrettyPrint
    }
}

extension Date {
    static func hours(_ hours: Double) -> Date {
        return Date(timeIntervalSinceNow: hours*3600)
    }
    static func minutes(_ minutes: Double) -> Date {
        return Date(timeIntervalSinceNow: minutes*60)
    }
    static func seconds(_ seconds: Double) -> Date {
        return Date(timeIntervalSinceNow: seconds)
    }
}