We store user information on the installation object. And when modifying or changing the data we save the installation.
The issue seems to be that the underlying InMemoryKeyValueStore setter does not support concurrency.
we have services and managers that handle different parts of our app and for example if a user logs in we set a pointer on the installation with the current user and save it async.
We also set the push permissions state async and save
we also set the channels and device token async and save
we check analytics tracking consent and if accepted async and save
now not all of these are fired off at the same time but in some cases 2-3 of these saves may be executed concurrently on app startup which causes the setter to be called concurrently and attempt to write to the underlying dictionary at the same time causing a runtime error as write operations on a dictionary may not be done concurrently.
mutating func set(_ object: T, for key: String) throws where T: Encodable {
let data = try encoder.encode(object)
storage[key] = data <- this is where it crashes
}
Perhaps modifying the storage[:] dictionary should be done on a custom serial queue so it cannot be modified concurrently on different threads?
Steps to reproduce
for index in 1..<10 {
Task.detached {
try await installation.save()
}
}
Actual Outcome
It crashes with runtime exception
Expected Outcome
Should not crash
Environment
Client
Parse Swift SDK version: 4.14.2
Xcode version: 14.3.1
Operating system (iOS, macOS, watchOS, etc.): iOS
Operating system version: 17.0.0
Server
Parse Server version: 4.3
Operating system: linux
Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): local
Database
System (MongoDB or Postgres): SQL
Database version: :shrug:
Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): local
Logs
0 0x0000000113814acb in objc_msgSend ()
1 0x000000011f261b9c in Dictionary.Variant.setValue(:forKey:) ()
2 0x000000011f224ae8 in Dictionary.subscript.setter ()
3 0x0000000116428ecd in InMemoryKeyValueStore.set<τ_00>(:for:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Storage/ParseKeyValueStore.swift:61
4 0x00000001164291bd in protocol witness for ParsePrimitiveStorable.set<τ_00>(:for:) in conformance InMemoryKeyValueStore ()
5 0x000000011651af87 in ParseStorage.set<τ_00>(:for:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Storage/ParseStorage.swift:56
6 0x00000001163ddc00 in static ParseInstallation.currentContainer.setter at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:257
7 0x00000001163decbf in static ParseInstallation.current.setter at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:302
8 0x00000001163e3b99 in static ParseInstallation.updateKeychainIfNeeded(_:deleting:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:495
9 0x0000000116417736 in ParseInstallation.command(method:ignoringCustomObjectIdConfig:options:callbackQueue:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation+async.swift:356
10 0x00000001163e76b0 in closure #1 in ParseInstallation.save(ignoringCustomObjectIdConfig:options:callbackQueue:completion:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:699
11 0x00000001163e8570 in partial apply for closure #1 in ParseInstallation.save(ignoringCustomObjectIdConfig:options:callbackQueue:completion:) ()
12 0x0000000116299580 in thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out τ_0_0) ()
13 0x000000011629a4b0 in partial apply for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out τ_0_0) ()
🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our Contribution Guide for how to make a pull request, or read our New Contributor's Guide if this is your first time contributing.
New Issue Checklist
Issue Description
We store user information on the installation object. And when modifying or changing the data we save the installation. The issue seems to be that the underlying InMemoryKeyValueStore setter does not support concurrency.
we have services and managers that handle different parts of our app and for example if a user logs in we set a pointer on the installation with the current user and save it async. We also set the push permissions state async and save we also set the channels and device token async and save we check analytics tracking consent and if accepted async and save
now not all of these are fired off at the same time but in some cases 2-3 of these saves may be executed concurrently on app startup which causes the setter to be called concurrently and attempt to write to the underlying dictionary at the same time causing a runtime error as write operations on a dictionary may not be done concurrently.
mutating func set(_ object: T, for key: String) throws where T: Encodable {
let data = try encoder.encode(object)
storage[key] = data <- this is where it crashes
}
Perhaps modifying the storage[:] dictionary should be done on a custom serial queue so it cannot be modified concurrently on different threads?
Steps to reproduce
for index in 1..<10 { Task.detached { try await installation.save() } }
Actual Outcome
It crashes with runtime exception
Expected Outcome
Should not crash
Environment
Client
4.14.2
14.3.1
iOS
17.0.0
Server
4.3
linux
local
Database
SQL
:shrug:
local
Logs
0 0x0000000113814acb in objc_msgSend ()
1 0x000000011f261b9c in Dictionary.Variant.setValue(:forKey:) ()
2 0x000000011f224ae8 in Dictionary.subscript.setter ()
3 0x0000000116428ecd in InMemoryKeyValueStore.set<τ_00>(:for:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Storage/ParseKeyValueStore.swift:61
4 0x00000001164291bd in protocol witness for ParsePrimitiveStorable.set<τ_00>(:for:) in conformance InMemoryKeyValueStore ()
5 0x000000011651af87 in ParseStorage.set<τ_00>(:for:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Storage/ParseStorage.swift:56
6 0x00000001163ddc00 in static ParseInstallation.currentContainer.setter at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:257
7 0x00000001163decbf in static ParseInstallation.current.setter at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:302
8 0x00000001163e3b99 in static ParseInstallation.updateKeychainIfNeeded(_:deleting:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:495
9 0x0000000116417736 in ParseInstallation.command(method:ignoringCustomObjectIdConfig:options:callbackQueue:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation+async.swift:356
10 0x00000001163e76b0 in closure #1 in ParseInstallation.save(ignoringCustomObjectIdConfig:options:callbackQueue:completion:) at /Users/alessio.diimperio/Documents/ios/Pods/ParseSwift/Sources/ParseSwift/Objects/ParseInstallation.swift:699
11 0x00000001163e8570 in partial apply for closure #1 in ParseInstallation.save(ignoringCustomObjectIdConfig:options:callbackQueue:completion:) ()
12 0x0000000116299580 in thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out τ_0_0) ()
13 0x000000011629a4b0 in partial apply for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out τ_0_0) ()