Skip to content

Commit 77c70ad

Browse files
ZhaoChaoqunclaude
andauthored
feat: add 2-second wall-clock timeout to Cloud Rewrite (#16)
Use withThrowingTaskGroup to race the API call against a 2s deadline. If the Cerebras API doesn't respond within 2 seconds, the original transcription text is used immediately — no more 12-second waits. Implementation: - TaskGroup race: API call vs Task.sleep(2s), first result wins - Loser is cancelled via group.cancelAll() (frees URLSession resources) - URLRequest.timeoutInterval lowered from 15s to 3s as safety net - Timeout events logged for monitoring - rewriteTimeout defined as named Duration constant Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent de7e837 commit 77c70ad

1 file changed

Lines changed: 33 additions & 2 deletions

File tree

Sources/CloudRewriteService.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ final class CloudRewriteService {
88
private let apiKeyProvider: () -> String?
99
private let endpoint: URL
1010

11+
/// Wall-clock timeout for the entire rewrite operation.
12+
/// If the API doesn't respond within this duration, the original text is used.
13+
private static let rewriteTimeout: Duration = .seconds(2)
14+
1115
/// Models to try in priority order. First available model wins.
1216
private static let preferredModels = [
1317
"qwen-3-235b-a22b-instruct-2507",
@@ -114,7 +118,31 @@ final class CloudRewriteService {
114118
}
115119

116120
do {
117-
return try await rewrite(text: text, apiKey: apiKey)
121+
return try await withThrowingTaskGroup(of: String.self) { group in
122+
// Race 1: the actual API call
123+
group.addTask { [self] in
124+
try await self.rewrite(text: text, apiKey: apiKey)
125+
}
126+
127+
// Race 2: timeout deadline
128+
group.addTask {
129+
try await Task.sleep(for: Self.rewriteTimeout)
130+
throw RewriteTimeoutError()
131+
}
132+
133+
// First completed result wins; cancel the loser
134+
guard let result = try await group.next() else {
135+
return text
136+
}
137+
group.cancelAll()
138+
return result
139+
}
140+
} catch is RewriteTimeoutError {
141+
cloudRewriteLogger.warning("Cloud rewrite timed out (\(Self.rewriteTimeout, privacy: .public)), using original text (\(text.count, privacy: .public) chars)")
142+
return text
143+
} catch is CancellationError {
144+
cloudRewriteLogger.debug("Cloud rewrite cancelled")
145+
return text
118146
} catch let error as URLError {
119147
cloudRewriteLogger.warning("Cloud rewrite network error: \(error.code.rawValue, privacy: .public). Fallback to original text")
120148
return text
@@ -132,7 +160,7 @@ final class CloudRewriteService {
132160

133161
var request = URLRequest(url: endpoint)
134162
request.httpMethod = "POST"
135-
request.timeoutInterval = 15
163+
request.timeoutInterval = 3 // Safety net; task group timeout (2s) is the primary enforcer
136164
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
137165
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
138166

@@ -265,3 +293,6 @@ private struct ModelListResponse: Decodable {
265293
let id: String
266294
}
267295
}
296+
297+
/// Thrown by the timeout racer to signal that the rewrite deadline has been exceeded.
298+
private struct RewriteTimeoutError: Error {}

0 commit comments

Comments
 (0)