kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Serializer crashing in a multi-threaded environment

Open monishsyed opened this issue 2 years ago • 0 comments

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)
            }
        }
    }

monishsyed avatar May 23 '22 19:05 monishsyed