kotlinx.serialization
kotlinx.serialization copied to clipboard
Serializer crashing in a multi-threaded environment
Issue: On iOS, attempting to concurrently deserialize multiple gateway responses on a background queue throws the following runtime exception and crashes the.
Crashed: com.apple.NSURLSession-delegate
0 MopEngine 0xca834 kfun:kotlinx.serialization.json.internal.StreamingJsonDecoder#<init>(kotlinx.serialization.json.Json;kotlinx.serialization.json.internal.WriteMode;kotlinx.serialization.json.internal.AbstractJsonLexer;kotlinx.serialization.descriptors.SerialDescriptor){} + 27 (StreamingJsonDecoder.kt:27)
1 MopEngine 0xb734c kfun:kotlinx.serialization.json.Json#decodeFromString(kotlinx.serialization.DeserializationStrategy<0:0>;kotlin.String){0§<kotlin.Any?>}0:0 + 99 (Json.kt:99)
2 MopEngine 0x26ac28 objc2kotlin.2995 + 19 (GatewaySerialization.kt:19)
3 ConsumerNetworking 0x2a71c CctEtaDTO.decodeETABody(_:) + 47 (CctEtaDTO.swift:47)
4 ConsumerNetworking 0x2a408 CctEtaDTO.mapping(map:) + 38 (CctEtaDTO.swift:38)
5 ConsumerNetworking 0x2a8cc protocol witness for BaseMappable.mapping(map:) in conformance CctEtaDTO + 20 (<compiler-generated>:20)
6 ObjectMapper 0x17268 Mapper.map(JSON:) + 106 (Mapper.swift:106)
7 ObjectMapper 0x1a92c partial apply for thunk for @callee_guaranteed (@guaranteed [String : Any]) -> (@out A?, @error @owned Error) + 24 (<compiler-generated>:24)
8 libswiftCore.dylib 0x1c0ce0 Sequence.compactMap<A>(_:) + 1224
9 ObjectMapper 0x15da0 Mapper.mapArray(JSONObject:) + 168 (Mapper.swift:168)
10 ObjectMapper 0xac10 static FromJSON.objectArray<A>(_:map:) + 172
Following is the deserialization code I'm using. I can't see through any non-thread safe that may be causing it - we are freezing the result, using @SharedImmutable annotation for json configs etc.
public object GatewayDeserializerFactory {
public fun provideGatewayDeserializer() : GatewayDeserializer =GatewayDeserializerImpl(JsonNonStrict)
}
@SharedImmutable
internal val JsonNonStrict = Json {
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
encodeDefaults = true
}
public interface GatewayDeserializer {
@Throws(DeserializationException::class)
public fun deserialize(jsonString: String): List<Student>
}
internal class GatewayDeserializerImpl(private val json: Json) : GatewayDeserializer {
@Throws(DeserializationException::class)
override fun deserialize(jsonString: String): List<Student> {
try {
val result: List<Student> =
json.decodeFromString(ListSerializer(Student.serializer()), jsonString)
return result.freeze()
} catch (throwable: Throwable) {
// Wrapping the whole throwable might lead to crashes like
// https://servicedesk.careempartner.com/browse/MOB-4400
val message = throwable.message
throw DeserializationException(message).freeze()
}
}
}
The crash can easily be reproduced on iOS using the following test
func testDeserializationRaceCondition() {
let jsonString = ""
let queue = DispatchQueue(label: "my_concurrent_queue", attributes: .concurrent)
for _ in 0..<5 {
queue.async {
let deserializer = GatewayDeserializerFactory().provideGatewayDeserializer()
let dto = try? deserializer.deserialize(jsonString: jsonString)
}
}
}