781flyingdutchman / background_downloader

Flutter plugin for file downloads and uploads
Other
154 stars 71 forks source link

iOS native AsyncLock `lock()` method causes hang #362

Closed Ansis100 closed 1 week ago

Ansis100 commented 3 weeks ago

The app can enter a broken state where any invokeMethod call that uses the HoldingQueue will cause the app to hang at that awaited function call.

I am not entirely sure how to reproduce the bug. My app mainly performs file uploads using background_downloader. It seems like the bug happens when work is being done by the uploader and the app loses its focus, e.g. is closed by the user or receives a phone call (based on my experience).

When the app is reopened, it locates the previous tasks in a custom DB to enqueue them again. When it tries to do so, it hangs on the invokeMethod call and gets stuck in this loop: https://github.com/781flyingdutchman/background_downloader/blob/21d5d2efc2a9c70e15214b08fb81826c2b156794/ios/Classes/HoldingQueue.swift#L241-L243

Here is the UploadTask received by the uploader when the native code fails (copied from the debugger):

UploadTask (
  UploadTask{
    taskId: 9defc63a-3b82-4f06-957d-d2697960b6e0, 
    url: https://webdav_host/iPhone/Videos/IMG_1255.MP4, 
    filename: 488F5F21-3B6E-4B05-8772-EDE745BAF3A5_L0_001_1724229688.388737_o_MOV_0170.mp4, 
    headers: {
      Authorization: Basic [REDACTED], 
      User-Agent: [REDACTED]
    }, 
    httpRequestMethod: PUT, 
    post: not null, 
    directory: private/var/mobile/Containers/Data/Application/51AA42C0-52FC-46C1-A578-D595A6393A59/tmp/.video, 
    baseDirectory: BaseDirectory.root, 
    group: backup, 
    updates: Updates.statusAndProgress, 
    requiresWiFi: false, 
    retries: 5, 
    retriesRemaining: 5, 
    allowPause: false, 
    priority: 5, 
    metaData: 270, 
    displayName: 
  } and fileField file, mimeType video/mp4 and fields {}
)

From what I can see, it doesn't look that different from a normal task which enqueues successfully:

UploadTask (
  UploadTask{
    taskId: 50165d03-9e23-4f4f-b020-20f7a35ab12d, 
    url: https://webdav_host/iPhone/Videos/IMG_1258.MP4, 
    filename: 87AD8BCE-1735-4018-9245-B3C1EF1A3DFC_L0_001_1724229690.162746_o_MOV_0463.mp4, 
    headers: {...}, 
    httpRequestMethod: PUT, 
    post: not null, 
    directory: private/var/mobile/Containers/Data/Application/4E98F7E6-ED81-4296-AC50-062B214FC3EB/tmp/.video, 
    baseDirectory: BaseDirectory.root, 
    group: backup, 
    updates: Updates.statusAndProgress, 
    requiresWiFi: false, 
    retries: 5, 
    retriesRemaining: 5, 
    allowPause: false, 
    priority: 5, 
    metaData: 1, 
    displayName: 
  } and fileField file, mimeType video/mp4 and fields {}
)

Is there any additional info I can provide to figure out what exactly might be causing this?

Ansis100 commented 3 weeks ago

I think I figured out what is causing this. I'm not entirely sure how to fix it because I am still not 100% familiar with the iOS codebase.

The processQueue method calls item.enqueue() on line 165. Before that it locks the state on line 153. Currently, any other methods that call lock() will wait to be unlocked.

https://github.com/781flyingdutchman/background_downloader/blob/21d5d2efc2a9c70e15214b08fb81826c2b156794/ios/Classes/HoldingQueue.swift#L153-L175

If the doEnqueue() returns false (e.g. if the file cannot be found) then enqueue() will attempt to clean up by calling taskFinished() on line 231.

https://github.com/781flyingdutchman/background_downloader/blob/21d5d2efc2a9c70e15214b08fb81826c2b156794/ios/Classes/HoldingQueue.swift#L227-L231

However, taskFinished() ALSO has a lock() on line 58, which prevents the method from ever executing because the call stack already contains a lock() in processQueue().

https://github.com/781flyingdutchman/background_downloader/blob/21d5d2efc2a9c70e15214b08fb81826c2b156794/ios/Classes/HoldingQueue.swift#L57-L58

As a result, the stateLock is forever locked and nothing can unlock it. @781flyingdutchman does this look right to you? I removed the lock call from taskFinished as a temporary solution and it seemed to fix the forever-lock.

lyio commented 2 weeks ago

I had this frequently occur when trying to cancel a task on iOS and was just about to open an issue :)

781flyingdutchman commented 1 week ago

Thanks for raising the issue and identifying the problem! Working on a fix

781flyingdutchman commented 1 week ago

Implemented in 8.5.4