Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update CDSHandler logic to v2 #39

Merged
merged 3 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 188 additions & 26 deletions Sources/Transifex/CDSHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ public typealias TXPullCompletionHandler = (TXTranslations, [Error]) -> Void

/// Handles the logic of a pull HTTP request to CDS for a certain locale code
class CDSPullRequest {

private static let MAX_RETRIES = 20

let code : String
let request : URLRequest
let session : URLSession
Expand Down Expand Up @@ -92,7 +89,7 @@ class CDSPullRequest {
case CDSHandler.HTTP_STATUS_CODE_ACCEPTED:
Logger.info("Received 202 response while fetching locale: \(self.code)")

if self.retryCount < CDSPullRequest.MAX_RETRIES {
if self.retryCount < CDSHandler.MAX_RETRIES {
self.retryCount += 1
self.perform(with: completionHandler)
}
Expand All @@ -112,11 +109,14 @@ class CDSPullRequest {

/// Handles communication with the Content Delivery Service.
class CDSHandler {
/// Max retries for both the pull and the push / job status requests
fileprivate static let MAX_RETRIES = 20

private static let CDS_HOST = "https://cds.svc.transifex.net"

private static let CONTENT_ENDPOINT = "content"
private static let INVALIDATE_ENDPOINT = "invalidate"

private static let FILTER_TAGS_PARAM = "filter[tags]"

fileprivate static let HTTP_STATUS_CODE_OK = 200
Expand All @@ -134,28 +134,61 @@ class CDSHandler {

/// Private structure that's used to parse the data received by the invalidate endpoint
private struct InvalidationResponseData: Decodable {
var status: String
var token: String
var count: Int
struct Data: Decodable {
var status: String
var token: String
var count: Int
}
var data: Data
}

/// Private structure that's used to parse the server response when pushing source strings
private struct PushResponseData: Decodable {
struct Links: Decodable {
var job: String
}
struct Data: Decodable {
var id: String
var links: Links
}
var data: Data
}

/// Private structure that's used to parse the server response when fetching the job status.
///
/// The errors field is available only in the 'completed' and 'failed' statuses and the details field is
/// available only in the 'completed' status.
private struct JobStatusResponseData: Decodable {
struct Data: Decodable {
var status: JobStatus
var errors: [JobError]?
var details: JobDetails?
}
var data: Data
}

private struct JobDetails: Decodable {
var created: Int
var updated: Int
var skipped: Int
var deleted: Int
var failed: Int
var errors: [PushResponseError]
}

private struct PushResponseError: Decodable {
private struct JobError: Decodable {
var status: String
var code: String
var title: String
var detail: String
var source: [String: String]
}

private enum JobStatus: String, Decodable {
case pending
case processing
case completed
case failed
}

/// A list of locale codes for the configured languages in the application
let localeCodes: [String]
Expand Down Expand Up @@ -327,13 +360,13 @@ class CDSHandler {
let response = try decoder.decode(InvalidationResponseData.self,
from: data)

if response.status != "success" {
if response.data.status != "success" {
Logger.error("Unsuccessful invalidation request")
completionHandler(false)
return
}

Logger.verbose("Invalidated \(response.count) translations from CDS for all locales in the project")
Logger.verbose("Invalidated \(response.data.count) translations from CDS for all locales in the project")
completionHandler(true)
}
catch {
Expand Down Expand Up @@ -387,21 +420,14 @@ class CDSHandler {
return
}

let statusCode = httpResponse.statusCode

if statusCode == CDSHandler.HTTP_STATUS_CODE_OK {
completionHandler(true)
return
}

Logger.error("HTTP Status error while pushing strings: \(statusCode)")

if statusCode == CDSHandler.HTTP_STATUS_CODE_FORBIDDEN {
if httpResponse.statusCode != CDSHandler.HTTP_STATUS_CODE_ACCEPTED {
Logger.error("HTTP Status error while pushing strings: \(httpResponse.statusCode)")
completionHandler(false)
return
}

guard let data = data else {
Logger.error("Error: No data received while pushing strings")
completionHandler(false)
return
}
Expand All @@ -417,8 +443,46 @@ class CDSHandler {
Logger.error("Error while decoding CDS push response: \(error)")
}

if let response = response {
for error in response.errors {
guard let finalResponse = response else {
completionHandler(false)
return
}

self.pollJobStatus(jobURL: finalResponse.data.links.job,
retryCount: 0,
completionHandler: completionHandler)

}.resume()
}

/// Polls the job status for CDSHandler.MAX_RETRIES times, or until it receives a failure or a
/// successful job status.
///
/// Warning: Do not call this method from the main thread as it sleeps for 1 second before performing
/// the actual network request.
///
/// - Parameters:
/// - jobURL: The relative job url (e.g. /jobs/content/123)
/// - retryCount: The current retry number
/// - completionHandler: The completion handler that informs the caller whether the job was
/// successful or not.
private func pollJobStatus(jobURL: String,
retryCount: Int,
completionHandler: @escaping (Bool) -> Void) {
// Delay the job status request by 1 second, so that the server can
// have enough time to process the job.
Thread.sleep(forTimeInterval: 1.0)

fetchJobStatus(jobURL: jobURL) {
jobStatus, jobErrors, jobDetails in
guard let finalJobStatus = jobStatus else {
Logger.error("Error: Fetch job status request failed")
completionHandler(false)
return
}

if let errors = jobErrors {
for error in errors {
Logger.error("""
\(error.title) (\(error.status) - \(error.code)):
\(error.detail)
Expand All @@ -427,8 +491,105 @@ Source:
""")
}
}

if let details = jobDetails {
Logger.verbose("""
created: \(details.created)
updated: \(details.updated)
skipped: \(details.skipped)
deleted: \(details.deleted)
failed: \(details.failed)
""")
}

switch finalJobStatus {
case .pending:
fallthrough
case .processing:
if retryCount < CDSHandler.MAX_RETRIES {
self.pollJobStatus(jobURL: jobURL,
retryCount: retryCount + 1,
completionHandler: completionHandler)
}
else {
Logger.error("Error: Max retries \(CDSHandler.MAX_RETRIES) reached")
completionHandler(false)
}
case .failed:
completionHandler(false)
case .completed:
completionHandler(true)
}
}
}

/// Peforms a single job status request to the CDS for a given job id and returns the response
/// asynchronously
///
/// - Parameters:
/// - jobURL: The relative job url (e.g. /jobs/content/123)
/// - completionHandler: A completion handler that contains the parsed response. The
/// response consists of the job status (which is nil in case of a failure), an optional array of errors
/// in case job failed or succeeded with errros and an optional structure of the job details in case job
/// was successful.
private func fetchJobStatus(jobURL: String,
completionHandler: @escaping (JobStatus?,
[JobError]?,
JobDetails?) -> Void) {
guard let cdsHostURL = URL(string: cdsHost) else {
Logger.error("Error: Invalid CDS host URL: \(cdsHost)")
completionHandler(nil, nil, nil)
return
}

Logger.verbose("Fetching job status for job: \(jobURL)...")

let baseURL = cdsHostURL
.appendingPathComponent(jobURL)
var request = URLRequest(url: baseURL)
request.httpMethod = "GET"
request.allHTTPHeaderFields = getHeaders(withSecret: true)

completionHandler(false)
session.dataTask(with: request) { (data, response, error) in
guard error == nil else {
Logger.error("Error retrieving job status: \(error!)")
completionHandler(nil, nil, nil)
return
}

guard let httpResponse = response as? HTTPURLResponse else {
Logger.error("Error retrieving job status: Not a valid HTTP response")
completionHandler(nil, nil, nil)
return
}

if httpResponse.statusCode != CDSHandler.HTTP_STATUS_CODE_OK {
Logger.error("HTTP Status error while retrieving job status: \(httpResponse.statusCode)")
completionHandler(nil, nil, nil)
return
}

guard let finalData = data else {
Logger.error("Error: No data received while retrieving job status")
completionHandler(nil, nil, nil)
return
}

let decoder = JSONDecoder()
var responseData: JobStatusResponseData? = nil

do {
responseData = try decoder.decode(JobStatusResponseData.self,
from: finalData)
}
catch {
Logger.error("Error while decoding CDS job status response: \(error)")
}

completionHandler(responseData?.data.status,
responseData?.data.errors,
responseData?.data.details)

}.resume()
}

Expand Down Expand Up @@ -471,8 +632,9 @@ Source:
etag: String? = nil) -> [String: String] {
var headers = [
"Accept-Encoding": "gzip",
"Content-Type": "application/json",
"X-NATIVE-SDK": "mobile/ios/\(TXNative.version)"
"Content-Type": "application/json; charset=utf-8",
"X-NATIVE-SDK": "mobile/ios/\(TXNative.version)",
"Accept-version": "v2"
]
if withSecret == true,
let secret = secret {
Expand Down
Loading