Skip to content

Fix Race conditions#359

Open
biovolt wants to merge 1 commit into
SDWebImage:masterfrom
biovolt:master
Open

Fix Race conditions#359
biovolt wants to merge 1 commit into
SDWebImage:masterfrom
biovolt:master

Conversation

@biovolt

@biovolt biovolt commented Sep 2, 2025

Copy link
Copy Markdown

I believe this fixes
#358

Summary by CodeRabbit

  • Bug Fixes
    • Improves stability of image loading by ensuring progress and completion callbacks are invoked consistently under concurrent scenarios.
    • Delivers progress updates reliably on the main thread, reducing missed or duplicated notifications.
    • Reduces rare race-related crashes or hangs during rapid callback changes.
  • Refactor
    • Internally synchronizes callback handling for more predictable behavior, with no changes to how you set or use callbacks.
@coderabbitai

coderabbitai Bot commented Sep 2, 2025

Copy link
Copy Markdown

Walkthrough

Introduces thread-safe storage and invocation for image loading callbacks in ImageManager by adding a private callback queue and backing closures. Public callback properties become computed with synchronized get/set. Progress and completion now capture local callback references and dispatch to the main thread, minimizing race conditions. No functional flow changes.

Changes

Cohort / File(s) Summary of Changes
Thread-safe callback handling
SDWebImageSwiftUI/Classes/ImageManager.swift
- Added callbackQueue and private _successBlock, _failureBlock, _progressBlock.
- Converted successBlock, failureBlock, progressBlock to computed properties synchronized via callbackQueue.
- Captured local callback copies for progress/completion before invoking on main thread.
- Maintains existing loading flow and API surface behavior.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant View
  participant ImageManager
  participant SDWebImage
  participant CallbackQueue as Callback Queue
  participant MainThread

  View->>ImageManager: set success/failure/progress blocks
  ImageManager->>CallbackQueue: sync set _success/_failure/_progress

  View->>ImageManager: load(url)
  ImageManager->>SDWebImage: start request

  SDWebImage-->>ImageManager: progress(received,total)
  ImageManager->>CallbackQueue: sync get _progress
  CallbackQueue-->>ImageManager: progressCallback
  alt progressCallback exists
    ImageManager->>MainThread: invoke progressCallback(received,total)
  end

  SDWebImage-->>ImageManager: completion(image,data,cacheType or error)
  ImageManager->>CallbackQueue: sync get _success/_failure
  CallbackQueue-->>ImageManager: successCallback/failureCallback
  alt success
    ImageManager->>MainThread: invoke successCallback(image,data,cacheType)
  else failure
    ImageManager->>MainThread: invoke failureCallback(error)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

In burrows of bytes, I twitch with delight,
Queued little callbacks, kept snug and tight.
No races to chase, no hare-brained despair—
Progress hops softly, completion with care.
On the main meadow, we nuzzle the view,
Thread-safe thumps: thump-thump—swift and true.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
SDWebImageSwiftUI/Classes/ImageManager.swift (3)

64-84: Nice containment for callback races; consider concurrent queue + barrier writes to reduce contention.

Your serial queue works; a lightweight upgrade improves read parallelism without sacrificing safety.

Apply:

-    private let callbackQueue = DispatchQueue(label: "ImageManager.callbacks", qos: .userInitiated)
+    private let callbackQueue = DispatchQueue(label: "ImageManager.callbacks",
+                                              qos: .userInitiated,
+                                              attributes: .concurrent)
@@
     var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)? {
         get { callbackQueue.sync { _successBlock } }
-        set { callbackQueue.sync { _successBlock = newValue } }
+        set { callbackQueue.sync(flags: .barrier) { _successBlock = newValue } }
     }
@@
     var failureBlock: ((Error) -> Void)? {
         get { callbackQueue.sync { _failureBlock } }
-        set { callbackQueue.sync { _failureBlock = newValue } }
+        set { callbackQueue.sync(flags: .barrier) { _failureBlock = newValue } }
     }
@@
     var progressBlock: ((Int, Int) -> Void)? {
         get { callbackQueue.sync { _progressBlock } }
-        set { callbackQueue.sync { _progressBlock = newValue } }
+        set { callbackQueue.sync(flags: .barrier) { _progressBlock = newValue } }
     }

117-123: Good: snapshot callback then hop to main. Align contract across all callbacks.

Progress is delivered on main; success/failure currently aren’t. Consider documenting or making them consistent (see next comment).

Would you like me to add doc comments clarifying callback threading guarantees?


135-152: Dispatch completion callbacks to main and avoid discouraged NSError() init.

Prevents UI-thread surprises and fixes SwiftLint discouraged_direct_init (hint at Line 151).

Apply:

-                    if let image = image {
-                        successCallback?(image, data, cacheType)
-                    } else {
-                        failureCallback?(error ?? NSError())
-                    }
+                    DispatchQueue.main.async {
+                        if let image = image {
+                            successCallback?(image, data, cacheType)
+                        } else {
+                            let defaultError = NSError(
+                                domain: "SDWebImageSwiftUI.ImageManager",
+                                code: -1,
+                                userInfo: [NSLocalizedDescriptionKey: "Image load failed with unknown error"]
+                            )
+                            failureCallback?(error ?? defaultError)
+                        }
+                    }

If existing users rely on background-thread completion, confirm before merging to avoid a breaking change. I can add a minor version note or a feature flag (e.g., deliverCompletionsOnMain).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ea24105 and ad8e7f2.

📒 Files selected for processing (1)
  • SDWebImageSwiftUI/Classes/ImageManager.swift (4 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
SDWebImageSwiftUI/Classes/ImageManager.swift

[Warning] 151-151: Discouraged direct initialization of types that can be harmful

(discouraged_direct_init)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Unit Test (tvOS)
  • GitHub Check: Build Library
  • GitHub Check: Unit Test (iOS)
  • GitHub Check: Cocoapods Lint
  • GitHub Check: Run Demo
var progressBlock: ((Int, Int) -> Void)?

// Thread-safe callback properties
private let callbackQueue = DispatchQueue(label: "ImageManager.callbacks", qos: .userInitiated)

@dreampiggy dreampiggy Sep 3, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ImageManager is not a shared instance. I remember it's just behaved as ObservedObject used for View (it like a ViewModel), so it's created everytime new URL is provided.

Does this (means, you create each DispatchQueue in each Manager instance) cause queue explosion ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point!
How do you propose I fix it?

GlennBrann pushed a commit to GlennBrann/SDWebImageSwiftUI that referenced this pull request Mar 6, 2026
Progress and completion callbacks from SDWebImage are invoked on
background threads. Writing ObservableObject-published properties
off the main thread is a data race with SwiftUI's rendering.

Changes:
- Remove per-property didSet dispatches (up to 5 render passes each load)
- Move entire progress block body into DispatchQueue.main.async so
  indicatorStatus.progress is always written on main thread
- Move entire completion block body into DispatchQueue.main.async so
  all property writes are atomic and on the correct thread
- Single objectWillChange.send() before all mutations -> one render pass

Avoids the per-instance DispatchQueue overhead raised in PR SDWebImage#359
while fixing the same threading bugs more directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants