Closed nicobayer closed 7 months ago
Hi @nicobayer, thank you for taking the time to report this issue. It is especially helpful to have the stack trace.
LDClient.get(...)
should be thread safe. All methods on a given instance of an LDClient
are also thread safe. I suspect this has less to do with threading and more to do with a held memory reference that the OS cleaned up while in the background. I'm investigating possible causes. Do you ever hold a reference to an LDClient
returned from LDClient.get(...)
any where in your code? If so, could you describe the lifecycle of that reference and the class holding that reference.
How many total occurrences have you seen in your crash monitoring tool?
Thank you!
@tanderson-ld we don't hold a reference to LDClient
, we always go with LDClient.get()
to get an instance.
We had 449 occurrences of this crash in the last 90 days.
I'm still looking into this but my guess is that LDClient.start()
happens at the same time as we are checking for a feature flag with LDClient.get().boolVariation(forKey:defaultValue:)
.
Will come back with additional information if I find anything else.
Thanks for your prompt response :)
That sounds like it could be a likely culprit. start(...)
needs to complete before flag evaluation calls are made when the SDK is not initialized, which may be the case coming from a background state. You can check isInitialized
to help debug.
If you have multiple threads, you may need to set up synchronization between start(...)
completing and the thread doing the evaluation. There is a flavor of start(...)
which supports a timeout. You can think of it as "start waiting up to this amount of time to get fresh flag data from the cloud, if timeout hit, proceed with cache flag data but still try to get fresh data". In any flavor you call in a multithreaded situation, you should wait for the completion callback before making the flag evaluation call.
I see, we are definitely not waiting for start()
to end before evaluating flags so will look for ways to make this happen.
Are you calling start(...)
when your app launches normally (not from the background)? Perhaps that app launch code path is single threaded and why you don't see the crash in clean launch / foreground usage cases?
Note: Keep in mind that if you structure your start(...)
to not wait for fresh flag data (perhaps launch time is super important), on first launch this may result in default flag values. There is really no perfect solution to that situation and is best handled by reactive programming so that when flag values are eventually fetched your user experience updates dynamically.
Correct, start()
is called in initial app launch event, not in background/foreground events.
We do not wait for LD to finish start()
call as we want to reduce risk of LD bottlenecking our startup process. As you said tradeoff would be getting outdated flag states which we are okay with as some of our UI refreshes on appear events.
We ended up adding a concurrent queue to synchronize LD usage (snippet below). We took a peak at the LDClient.swift
and noticed LDClient.instances
private dictionary, swift collections are not thread-safe so we think that is our root cause for the crash. Probably not the only solution but we feel like this should fix the crash and also allow us to continue using LD safely from any thread.
I'm going to close the ticket but feel free to re-open if you want to continue discussion. We will know if this fixed the crash a few months from now as we have a long release cycle.
Thanks!
let syncQueue = DispatchQueue(label: "com.climate.launchDarklySyncQueue", qos: .userInitiated, attributes: .concurrent)
func getClient() -> LDClient? {
var client: LDClient?
syncQueue.sync {
client = LDClient.get()
}
return client
}
func start(user: FeatureFlagsUser?, completion: FeatureFlagStartCompletion?) {
syncQueue.async(flags: .barrier) {
LDClient.start(...) {
DispatchQueue.main.async { completion?(...) }
}
}
}
Describe the bug App crashes on launch. Stack trace lead to function
LDClient.get(environment:)
.To reproduce We have been unable to reproduce the crash in a debug environment.
Expected behavior Should not crash.
Logs No logs available.
SDK version 8.0.1
Language version, developer tools Xcode 14.1 / Swift 5
OS/platform Crash only happens on iOS 17.
Additional context Occurrence is very low, less than 0.01% of sessions impacted. Crash happens when the app is woken up to perform a background refresh of data (using BGAppRefreshTask). We believe this is a threading issue where there is multiple threads attempting to use LDClient at the same time to check for feature flag status and potentially other functionality. Are all functions safe to use from any thread?