Skip to content

Commit 8d13205

Browse files
authored
Merge pull request #219 from ojhurst/fix/transcription-hard-timeout
Fix transcription hanging indefinitely when Groq stalls after TCP accept
2 parents a322dfb + c797939 commit 8d13205

1 file changed

Lines changed: 105 additions & 4 deletions

File tree

Sources/TranscriptionService.swift

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,42 @@ class TranscriptionService {
5353
throw CancellationError()
5454
}
5555

56-
do {
57-
return try await transcribeAudio(fileURL: fileURL)
58-
} catch let urlError as URLError where urlError.code == .timedOut {
59-
throw TranscriptionError.transcriptionTimedOut(transcriptionTimeoutSeconds)
56+
let timeoutSeconds = transcriptionTimeoutSeconds
57+
let raceState = TranscriptionTimeoutRaceState()
58+
59+
return try await withTaskCancellationHandler {
60+
try await withCheckedThrowingContinuation { continuation in
61+
raceState.setContinuation(continuation)
62+
63+
let transcriptionTask = Task { [weak self] in
64+
do {
65+
guard let self else {
66+
throw TranscriptionError.transcriptionFailed("Transcription service deallocated")
67+
}
68+
let result = try await self.transcribeAudio(fileURL: fileURL)
69+
raceState.finish(.success(result))
70+
} catch {
71+
raceState.finish(.failure(Self.transcriptionTimeoutErrorIfNeeded(
72+
error,
73+
timeoutSeconds: timeoutSeconds
74+
)))
75+
}
76+
}
77+
78+
let timeoutTask = Task {
79+
do {
80+
try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
81+
raceState.finish(.failure(TranscriptionError.transcriptionTimedOut(timeoutSeconds)))
82+
} catch is CancellationError {
83+
} catch {
84+
raceState.finish(.failure(error))
85+
}
86+
}
87+
88+
raceState.setTasks([transcriptionTask, timeoutTask])
89+
}
90+
} onCancel: {
91+
raceState.cancel()
6092
}
6193
}
6294

@@ -208,6 +240,16 @@ class TranscriptionService {
208240
}
209241
}
210242

243+
private static func transcriptionTimeoutErrorIfNeeded(
244+
_ error: Error,
245+
timeoutSeconds: TimeInterval
246+
) -> Error {
247+
if let urlError = error as? URLError, urlError.code == .timedOut {
248+
return TranscriptionError.transcriptionTimedOut(timeoutSeconds)
249+
}
250+
return error
251+
}
252+
211253
private static func normalizedBaseURL(from baseURL: String) throws -> URL {
212254
let trimmed = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
213255
guard !trimmed.isEmpty else {
@@ -341,6 +383,65 @@ enum TranscriptionError: LocalizedError {
341383
}
342384
}
343385

386+
private final class TranscriptionTimeoutRaceState {
387+
private let lock = NSLock()
388+
private var didFinish = false
389+
private var continuation: CheckedContinuation<String, Error>?
390+
private var tasks: [Task<Void, Never>] = []
391+
392+
func setContinuation(_ continuation: CheckedContinuation<String, Error>) {
393+
lock.lock()
394+
if didFinish {
395+
lock.unlock()
396+
continuation.resume(throwing: CancellationError())
397+
return
398+
}
399+
400+
self.continuation = continuation
401+
lock.unlock()
402+
}
403+
404+
func setTasks(_ tasks: [Task<Void, Never>]) {
405+
lock.lock()
406+
if didFinish {
407+
lock.unlock()
408+
tasks.forEach { $0.cancel() }
409+
return
410+
}
411+
412+
self.tasks = tasks
413+
lock.unlock()
414+
}
415+
416+
func finish(_ result: Result<String, Error>) {
417+
lock.lock()
418+
guard !didFinish else {
419+
lock.unlock()
420+
return
421+
}
422+
423+
didFinish = true
424+
let continuation = self.continuation
425+
self.continuation = nil
426+
let tasks = self.tasks
427+
self.tasks = []
428+
lock.unlock()
429+
430+
tasks.forEach { $0.cancel() }
431+
432+
switch result {
433+
case .success(let value):
434+
continuation?.resume(returning: value)
435+
case .failure(let error):
436+
continuation?.resume(throwing: error)
437+
}
438+
}
439+
440+
func cancel() {
441+
finish(.failure(CancellationError()))
442+
}
443+
}
444+
344445
private struct PreparedUploadAudio {
345446
let fileURL: URL
346447
let deleteOnCleanup: Bool

0 commit comments

Comments
 (0)