contentful / contentful.swift

A delightful Swift interface to Contentful's content delivery API.
MIT License
202 stars 82 forks source link

Client.jsonDecoderBuilder is not thread-safe #388

Open jshier opened 1 year ago

jshier commented 1 year ago

Running with the thread sanitizer, occasionally multiple requests will trigger a thread sanitizer warning. For example:

==================
WARNING: ThreadSanitizer: data race (pid=7186)
  Read of size 8 at 0x000136232208 by thread T9:
    #0 Contentful.JSONDecoderBuilder.build() -> Foundation.JSONDecoder <null> (Contentful:arm64+0x6b880) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #1 Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2aad4) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #2 Contentful.Client.fetchData(for: Contentful.AssetProtocol, with: Swift.Array<Contentful.ImageOption>, then: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> Swift.Optional<__C.NSURLSessionDataTask> <null> (Contentful:arm64+0x34f78) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.fetchImage(for: Contentful.Asset, with: Swift.Array<Contentful.ImageOption>, then: (Swift.Result<__C.UIImage, Swift.Error>) -> ()) -> Swift.Optional<__C.NSURLSessionDataTask> <null> (Contentful:arm64+0x3762c) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 closure #1 (Swift.CheckedContinuation<__C.UIImage, Swift.Error>) -> () in (extension in App):Contentful.Client.fetchImage(from: Contentful.Asset) async throws -> __C.UIImage <null> (App:arm64+0x10026f4a8) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #5 partial apply forwarder for closure #1 (Swift.CheckedContinuation<__C.UIImage, Swift.Error>) -> () in (extension in App):Contentful.Client.fetchImage(from: Contentful.Asset) async throws -> __C.UIImage <null> (App:arm64+0x100270278) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #6 merged closure #1 (Swift.UnsafeContinuation<A, Swift.Never>) -> () in Swift.withCheckedContinuation<A>(function: Swift.String, _: (Swift.CheckedContinuation<A, Swift.Never>) -> ()) async -> A <null> (libswift_Concurrency.dylib:arm64+0x6250) (BuildId: 3aad7af116ba3c209def62b20fb04cbc32000000200000000700000000040f00)

  Previous write of size 8 at 0x000136232208 by thread T12:
    #0 Contentful.Client.localizationContext.setter : Swift.Optional<Contentful.LocalizationContext> <null> (Contentful:arm64+0x24044) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #1 closure #1 (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> () in Contentful.Client.fetchCurrentSpaceLocales(then: (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2f3ac) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #2 partial apply forwarder for closure #1 (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> () in Contentful.Client.fetchCurrentSpaceLocales(then: (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2f580) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.(handleJSON in _558EF92E0F1A7A570BADF83F33C4E0F3)<A where A: Swift.Decodable>(data: Foundation.Data, completion: (Swift.Result<A, Swift.Error>) -> ()) -> () <null> (Contentful:arm64+0x31a74) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 closure #1 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x29eb0) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #5 partial apply forwarder for closure #1 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a044) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #6 closure #2 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a45c) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #7 partial apply forwarder for closure #2 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a6e8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #8 closure #1 @Sendable (Swift.Optional<Foundation.Data>, Swift.Optional<__C.NSURLResponse>, Swift.Optional<Swift.Error>) -> () in Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2c2d8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #9 partial apply forwarder for closure #1 @Sendable (Swift.Optional<Foundation.Data>, Swift.Optional<__C.NSURLResponse>, Swift.Optional<Swift.Error>) -> () in Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2cc78) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #10 reabstraction thunk helper from @escaping @callee_guaranteed @Sendable (@guaranteed Swift.Optional<Foundation.Data>, @guaranteed Swift.Optional<__C.NSURLResponse>, @guaranteed Swift.Optional<Swift.Error>) -> () to @escaping @callee_unowned @convention(block) @Sendable (@unowned Swift.Optional<__C.NSData>, @unowned Swift.Optional<__C.NSURLResponse>, @unowned Swift.Optional<__C.NSError>) -> () <null> (Contentful:arm64+0x2cdf4) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #11 <null> <null> (CFNetwork:arm64+0x7ca4) (BuildId: d5424365b1e13893a3d79ba98e14b4dc32000000200000000700000000001100)
    #12 _dispatch_client_callout <null> (libdispatch.dylib:arm64+0x5978) (BuildId: 2c71a955e8ee34b580f395d8e33b36ea32000000200000000700000000001100)

  Location is heap block of size 80 at 0x0001362321e0 allocated by main thread:
    #0 __sanitizer_mz_malloc <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x53800) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #1 _malloc_zone_malloc_instrumented_or_legacy <null> (libsystem_malloc.dylib:arm64+0x20d64) (BuildId: 41f0f02c8fca30a4905cf788fb82278332000000200000000700000000001100)
    #2 Contentful.Client.init(spaceId: Swift.String, environmentId: Swift.String, accessToken: Swift.String, host: Swift.String, clientConfiguration: Contentful.ClientConfiguration, sessionConfiguration: __C.NSURLSessionConfiguration, persistenceIntegration: Swift.Optional<Contentful.PersistenceIntegration>, contentTypeClasses: Swift.Optional<Swift.Array<Contentful.EntryDecodable.Type>>) -> Contentful.Client <null> (Contentful:arm64+0x260f8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.__allocating_init(spaceId: Swift.String, environmentId: Swift.String, accessToken: Swift.String, host: Swift.String, clientConfiguration: Contentful.ClientConfiguration, sessionConfiguration: __C.NSURLSessionConfiguration, persistenceIntegration: Swift.Optional<Contentful.PersistenceIntegration>, contentTypeClasses: Swift.Optional<Swift.Array<Contentful.EntryDecodable.Type>>) -> Contentful.Client <null> (Contentful:arm64+0x25c38) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 App.CMS.() -> App.CMS(in _5EFF18E815206C1CAC3F3F55493FA824).init() -> App.CMS <null> (App:arm64+0x100267ab4) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #5 App.CMS.__allocating_init() -> App.CMS <null> (App:arm64+0x10026749c) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #6 one-time initialization function for shared <null> (App:arm64+0x100267414) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #7 dispatch_once <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7c6a0) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #8 dispatch_once_f <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7c73c) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #9 App.CMS.shared.unsafeMutableAddressor : App.CMS <null> (App:arm64+0x100267548) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #10 App.AppDelegate.init() -> App.AppDelegate <null> (App:arm64+0x100510410) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #11 @objc App.AppDelegate.init() -> App.AppDelegate <null> (App:arm64+0x100510724) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #12 _UIApplicationMainPreparations <null> (UIKitCore:arm64+0xb856c0) (BuildId: 0732a1aeb6d8373d88a23dc5a63e7c4e32000000200000000700000000001100)
    #13 <null> <null> (0x000107b3d514)

  Thread T9 (tid=105081, running) is a GCD worker thread

  Thread T12 (tid=105154, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race (/Users/jshier/Library/Developer/CoreSimulator/Devices/8069F768-73E0-4399-A9BF-E0491A89466F/data/Containers/Bundle/Application/F6F762D0-FB83-42B5-A7DE-DF8E79DA7827/App.app/Frameworks/Contentful.framework/Contentful:arm64+0x6b880) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00) in Contentful.JSONDecoderBuilder.build() -> Foundation.JSONDecoder+0x2ac

Only modification here is that I've wrapped fetchImage in Swift concurrency, but that doesn't change the underly safety issue.

Fundamentally, the jsonDecoderBuilder property is unsafe because it's mutable yet accessible from multiple threads.

There are multiple possible solutions here but the simplest is likely a lock wrapper to provide safe access. I don't see such a construct in the project already, so I'm just reaching out to see if there's another preferred solution before I try to get the project fully building and testable locally.