Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in unregisterSuperProperty and currentSuperProperties #622

Open
daleswift opened this issue Jan 16, 2024 · 1 comment
Open

Crash in unregisterSuperProperty and currentSuperProperties #622

daleswift opened this issue Jan 16, 2024 · 1 comment

Comments

@daleswift
Copy link

Repeated calls to currentSuperProperties and unregisterSuperProperty can cause a crash.

We are seeing this occur frequently in our production app.

Thread 22 Crashed:
0   libsystem_kernel.dylib               0x00000001e231dfbc __pthread_kill + 8
1   libsystem_c.dylib                    0x00000001a2c65b90 abort + 176
2   libswiftCore.dylib                   0x0000000194347064 swift::fatalErrorv(unsigned int, char const*, char*) + 124
3   libswiftCore.dylib                   0x0000000194347084 swift::fatalError(unsigned int, char const*, ...) + 28
4   libswiftCore.dylib                   0x000000019434ba84 swift_deallocClassInstance + 304
5   libswiftCore.dylib                   0x000000019434b914 _swift_release_dealloc + 52
6   libswiftCore.dylib                   0x000000019434cfb0 bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 132
7   nucleus-smart                        0x00000001009909d8 closure #1 () -> () in Mixpanel.MixpanelInstance.unregisterSuperProperty(Swift.String) -> () (<compiler-generated>:0)
8   nucleus-smart                        0x00000001009858cc reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @escaping @callee_unowned @convention(block) () -> () (<compiler-generated>:0)
9   libdispatch.dylib                    0x00000001a2bab6a8 _dispatch_call_block_and_release + 28
10  libdispatch.dylib                    0x00000001a2bad300 _dispatch_client_callout + 16
11  libdispatch.dylib                    0x00000001a2bb4894 _dispatch_lane_serial_drain + 744
12  libdispatch.dylib                    0x00000001a2bb53c4 _dispatch_lane_invoke + 376
13  libdispatch.dylib                    0x00000001a2bc0004 _dispatch_root_queue_drain_deferred_wlh + 284
14  libdispatch.dylib                    0x00000001a2bbf878 _dispatch_workloop_worker_thread + 400
15  libsystem_pthread.dylib              0x0000000204bd7964 _pthread_wqthread + 284
16  libsystem_pthread.dylib              0x0000000204bd7a04 start_wqthread + 4

Diagnosis

In most cases MixPanelInstance references superProperties from a block executed from the trackingQueue and under readWriteLock and so the reference is thread safe. As in registerSuperProperties:

    public func registerSuperProperties(_ properties: Properties) {
        trackingQueue.async { [weak self] in
            guard let self = self else { return }
            let updatedSuperProperties = self.trackInstance.registerSuperProperties(properties,
                                                                                    superProperties: self.superProperties)
            self.readWriteLock.write {
                self.superProperties = updatedSuperProperties
            }
            self.readWriteLock.read {
                MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name)
            }
        }
    }

However, currentSuperProperties only protects superProperties with readWriteLock:

    public func currentSuperProperties() -> [String: Any] {
        var properties = InternalProperties()
        self.readWriteLock.read {
            properties = superProperties
        }
        return properties
    }

and unregisterSuperProperty only protects superProperties with trackingQueue:

    public func unregisterSuperProperty(_ propertyName: String) {
        trackingQueue.async { [weak self] in
            guard let self = self else { return }
            self.superProperties = self.trackInstance.unregisterSuperProperty(propertyName,
                                                                              superProperties: self.superProperties)
            MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name)
        }
    }

this causes a crash when both unregisterSuperProperty and currentSuperProperties are called from different threads at the same time.

The following unit test simulates the problem:

    func testCrash() {
        let token = randomId()
        let mixPanel = Mixpanel.initialize(token: token, trackAutomaticEvents: false)
        let property = "MySuperProperty"
        let queue1 = DispatchQueue(label: "queue1", qos: .default)

        for _ in 0...10000 {
            mixPanel.unregisterSuperProperty(property)
            mixPanel.registerSuperProperties([property: "1"])
        }

        queue1.async {
            while true {
                _ = mixPanel.currentSuperProperties()
            }
        }

        let expect = expectation(description: "tracking finished")
        mixPanel.trackingQueue.async {
            expect.fulfill()
        }
        wait(for: [expect])
    }

    func randomId() -> String {
        return String(format: "%08x%08x", arc4random(), arc4random())
    }

Solution

The solution should be straightforward. Reference superProperties under readWriteLock in unregisterSuperProperty and similarly in updateSuperProperty

@daleswift
Copy link
Author

Submitted PR #623

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant