0

I receive the following warning:

SWIFT TASK CONTINUATION MISUSE: saveAndClose() leaked its continuation!

Inside for loop, after executing few items, at one of the item, in save and close function, it is blocked, and says above error, and not moving to next item, so not able to return result.

What could be wrong here, and is there any way to optimize any of these snippets?

private func processTags(reqItems: [FTShelfItemProtocol], selectedTags: [String]) async throws -> FTShelfTagsResult { let items: [FTDocumentItemProtocol] = reqItems.filter({ ($0.URL.downloadStatus() == .downloaded) }).compactMap({ $0 as? FTDocumentItemProtocol })

     var totalTagItems: [FTShelfTagsItem] = [FTShelfTagsItem]()

     for case let item in items where item.documentUUID != nil {
         guard let docUUID = item.documentUUID else { continue }//, item.URL.downloadStatus() == .downloaded else { continue }
         let destinationURL = FTDocumentCache.shared.cachedLocation(for: docUUID)
         print(destinationURL.path)
         // move to post processing phace
         do {
             let document = await FTNoteshelfDocument(fileURL: destinationURL)
             let isOpen = try await document.openDocument(purpose: FTDocumentOpenPurpose.read)
             if isOpen {
                 let tags = await document.documentTags()
                 let considerForResult = selectedTags.allSatisfy(tags.contains(_:))
                 if considerForResult && !tags.isEmpty {
                     var tagsBook = FTShelfTagsItem(shelfItem: item, type: .book)
                     tagsBook.tags = tags
                     totalTagItems.append(tagsBook)
                 }
             }

             let tagsPage = await document.fetchSearchTagsPages(shelfItem: item, selectedTags: selectedTags)
             totalTagItems.append(contentsOf: tagsPage)
             _ = await document.saveAndClose()
         } catch {
             cacheLog(.error, error, destinationURL.lastPathComponent)
         }
     }
     cacheLog(.success, totalTagItems.count)
     let result = FTShelfTagsResult(tagsItems: totalTagItems)
     return result
 }


func saveAndClose() async -> Bool {
    return await withCheckedContinuation({ continuation in
        self.saveAndCloseWithCompletionHandler { isSuccess in
            continuation.resume(returning: isSuccess)
        }
    })
}

func saveAndCloseWithCompletionHandler(_ onCompletion :((Bool) -> Void)?)
{
    FTCLSLog("Doc: Save and Close");
    self.prepareForClosing();
    self.saveDocument { (saveSuccess) in
        if(saveSuccess) {
            self.closeDocument(completionHandler: { (_) in
                onCompletion?(saveSuccess);
            });
        }
        else {
            onCompletion?(saveSuccess);
        }
    }
}

    func saveDocument(completionHandler : ((Bool) -> Void)?)
    {
        if(self.openPurpose == .read) {
            FTLogError("Doc Saved in Readonly");
            completionHandler?(true);
            return;
        }
        if(self.hasAnyUnsavedChanges) {
            (self.delegate as? FTNoteshelfDocumentDelegate)?.documentWillStartSaving(self);
        }
        FTCLSLog("Doc: Save");
        #if !NS2_SIRI_APP
        self.recognitionCache?.saveRecognitionInfoToDisk(forcibly: true);
        if(self.hasAnyUnsavedChanges) {
            if let cache = self.recognitionCache, let cachePlist = cache.recognitionCachePlist() {
                let mutableDict = NSMutableDictionary.init(dictionary: cachePlist.contentDictionary);
                self.recognitionInfoPlist()?.updateContent(mutableDict);
            }
            if let cache = self.recognitionCache, let cachePlist = cache.visionRecognitionCachePlist() {
                let mutableDict = NSMutableDictionary.init(dictionary: cachePlist.contentDictionary);
                self.visionRecognitionInfoPlist()?.updateContent(mutableDict);
            }

            //This was added in version 6.2, when we removed the bounding rect from the Segment level storage.
            updateDocumentVersionToLatest()
        }
        #endif

        super.save { (success) in
            if(success) {
                self.previousFileModeificationDate = self.fileModificationDate;
                let pages = self.pages();
                for eachPage in pages {
                    eachPage.isDirty = false;
                }
            }
            completionHandler?(success);
        }
    }
    
    func closeDocument(completionHandler: ((Bool) -> Void)?)
    {
         FTCLSLog("Doc: Close");
        #if !NS2_SIRI_APP
        self.recognitionCache?.saveRecognitionInfoToDisk(forcibly: true)
        #endif
        super.close { (success) in
            self.removeObservers();
            completionHandler?(success);
        }
    }


Need fix to avoid -

SWIFT TASK CONTINUATION MISUSE: saveAndClose() leaked its continuation!

and to return result properly after executing all items of for loop.

  • 1
    That error usually means that you are resuming twice, my guess is that completion handler is returning twice – lorem ipsum Jun 21 '23 at 11:42
  • https://stackoverflow.com/questions/68145462/swift-task-continuation-misuse-leaked-its-continuation-for-delegate Check the code of `saveAndClose()` and/or share it – Larme Jun 21 '23 at 11:43
  • saveAndClose() is already there, please verify – Narayana MV Jun 21 '23 at 12:40
  • 1
    Oh sorry, it was hidden in the scroll. Now show the code of `saveAndCloseWithCompletionHandler()`? It seems that the handler might be called multiple times or not once. – Larme Jun 21 '23 at 14:09
  • @Larme added code for saveAndCloseWithCompletionHandler. – Narayana MV Jun 22 '23 at 09:28
  • Can `saveDocument()` & `closeDocument()` completion be called multiple times or not once? For instance, if there is a case where `closeDocument()` doesn't call it `completionHandler`, then `onCompletion?(saveSuccess);` will never be called. – Larme Jun 22 '23 at 09:36
  • Your `saveAndCloseWithCompletionHandler` calls yet other custom functions (e.g. `saveDocument`, `closeDocument`, etc.). Your problem is related to how you are handling the continuation that you are passing through this closure. We can’t help you identify the source of the problem without seeing a [MRE](https://stackoverflow.com/help/minimal-reproducible-example), or at least all the code related to the handling of the closures in your code. – Rob Jun 22 '23 at 12:53

1 Answers1

0

Consider the error:

SWIFT TASK CONTINUATION MISUSE: saveAndClose() leaked its continuation!

That means that you have some path of execution that never called its completion handler, and thus the continuation was leaked. (It is not that you called the completion handler too many times, as that is different error message.)

I do not see an obvious path of execution where you neglect to call the completion handler. The problem would not appear to be in the code you have shared with us thus far: But you also have custom methods that have their own completion handlers, such as saveDocument and closeDocument that you have not shared with us, so perhaps one of them has a path of execution where it fails to call its completion handler. As such, we cannot say where precisely the problem rests in your case. But, bottom line, you likely have not called the completion handler somewhere. You may have to insert some logging statements and/or breakpoints and see if you can diagnose where the relevant completion handler is failing to be called.


Consider this reproducible example of the problem. The following completion-handler-based routine that returns a square root of some number after 1 second (I am just simulating some legacy, asynchronous process):

private func legacySquareRoot(of value: Double, completion: @escaping (Double) -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        if value >= 0 {
            completion(sqrt(value))
        }
    }
}

Clearly, I do not want to calculate the square root of a negative number. But the above is incorrect because not every path of execution will call its completion handler. (This is one of the reasons we prefer Swift concurrency over completion handler patterns, as this problem cannot occur in native Swift concurrency code.)

Anyway, let us presume that we wrote an async wrapper for this legacy function:

func squareRoot(of value: Double) async -> Double {
    await withCheckedContinuation { continuation in
        legacySquareRoot(of: value) { result in
            continuation.resume(returning: result)
        }
    }
}

If I call this with a positive value, everything is fine:

let value = await experiment.squareRoot(of: 2)
print(value)                                    // 1.4142135623730951

But if I call it with a negative value, I get the “leaked its continuation” error (because my legacy function had an error, only calling the closure if the values were positive):

let value = await experiment.squareRoot(of: -2)
print(value)                                    // SWIFT TASK CONTINUATION MISUSE: squareRoot(of:) leaked its continuation!

Clearly, the fix is that every path of execution must call its completion handler.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I have completion handlers for save and close document functions also. Now edited my post(added those functions too). Can you have a look if you can help if you find any? – Narayana MV Jul 06 '23 at 12:07
  • Still not jumping out at me. But failing to resume the continuation is undoubtedly the problem. Maybe `super.save` or `super.close` are possibly not calling their completion handler in some edge case? You have to track it all the way down. Or add logging statements to figure out in what scenario the closure is not getting called. Or just refactor all the code to use async-await (rather than just this top-level method) and the problem goes away. – Rob Jul 06 '23 at 20:01