Add constructor annotation for evolve selection

Current implementation is such that if we find *a* constructor that
works for us we use that, this is of course rather non deterministic, it
also means we may not select the best constructor

for example

old versions had constructors that took

V1: A B
V2: A B C D

if current version, V3 is

V3: A B C D E

which provides secondary constructors for the above then there is a
chance we'd de-serialise objects that were serialised as V2 using the V1
constructor and thus throw away information we don't need to

Additional Changes:

Fixes following rebase onto master
This commit is contained in:
Katelyn Baker 2017-08-23 13:57:28 +01:00
parent 7894e723e8
commit 299b8026c7
13 changed files with 259 additions and 5 deletions

View File

@ -0,0 +1,11 @@
package net.corda.core.serialization
/**
* This annotation is a marker to indicate which secondary constructors shuold be considered, and in which
* order, for evolving objects during their deserialisation.
*/
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class CordaSerializerConstructor(val version: Int)

View File

@ -1,10 +1,12 @@
package net.corda.nodeapi.internal.serialization.amqp package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializerConstructor
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type import java.lang.reflect.Type
import java.io.NotSerializableException import java.io.NotSerializableException
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.javaType
/** /**
@ -48,15 +50,20 @@ class EvolutionSerializer(
val oldArgumentSet = oldArgs.map { Pair (it.key, it.value) } val oldArgumentSet = oldArgs.map { Pair (it.key, it.value) }
var maxConstructorVersion = Integer.MIN_VALUE
var constructor: KFunction<Any>? = null
clazz.kotlin.constructors.forEach { clazz.kotlin.constructors.forEach {
if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) })) { val version = it.findAnnotation<CordaSerializerConstructor>()?.version ?: Integer.MIN_VALUE
return it if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) &&
version > maxConstructorVersion) {
constructor = it
maxConstructorVersion = version
} }
} }
// if we didn't get an exact match revert to existing behaviour, if the new parameters // if we didn't get an exact match revert to existing behaviour, if the new parameters
// are not mandatory (i.e. nullable) things are fine // are not mandatory (i.e. nullable) things are fine
return constructorForDeserialization(type) return constructor ?: constructorForDeserialization(type)
} }
/** /**

View File

@ -53,8 +53,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
} }
companion object { companion object {
fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer { fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
readMethod.isAccessible = true readMethod?.isAccessible = true
if (SerializerFactory.isPrimitive(resolvedType)) { if (SerializerFactory.isPrimitive(resolvedType)) {
return when(resolvedType) { return when(resolvedType) {
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod) Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)

View File

@ -1,6 +1,8 @@
package net.corda.nodeapi.internal.serialization.amqp package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.CordaSerializerConstructor
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.NotSerializableException import java.io.NotSerializableException
@ -178,6 +180,7 @@ class EvolvabilityTests {
@Suppress("UNUSED") @Suppress("UNUSED")
data class CC (val a: Int, val b: String) { data class CC (val a: Int, val b: String) {
@CordaSerializerConstructor(1)
constructor (a: Int) : this (a, "hello") constructor (a: Int) : this (a, "hello")
} }
@ -188,6 +191,168 @@ class EvolvabilityTests {
assertEquals ("hello", deserializedCC.b) assertEquals ("hello", deserializedCC.b)
} }
@Test(expected = NotSerializableException::class)
@Suppress("UNUSED")
fun addMandatoryFieldWithAltConstructorUnAnnotated() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated")
val f = File(path.toURI())
@Suppress("UNUSED_VARIABLE")
val A = 1
// Original version of the class as it was serialised
//
// data class CC(val a: Int)
// val scc = SerializationOutput(sf).serialize(CC(A))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
data class CC (val a: Int, val b: String) {
// constructor annotation purposefully omitted
constructor (a: Int) : this (a, "hello")
}
// we expect this to throw as we should not find any constructors
// capable of dealing with this
DeserializationInput(sf).deserialize(SerializedBytes<CC>(f.readBytes()))
}
@Test
fun addMandatoryFieldWithAltReorderedConstructor() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor")
val f = File(path.toURI())
val A = 1
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
@Suppress("UNUSED")
data class CC (val a: Int, val b: Int, val c: String, val d: String) {
// ensure none of the original parameters align with the initial
// construction order
@CordaSerializerConstructor(1)
constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble")
}
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (A, deserializedCC.a)
assertEquals (B, deserializedCC.b)
assertEquals (C, deserializedCC.c)
assertEquals ("wibble", deserializedCC.d)
}
@Test
fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval")
val f = File(path.toURI())
val A = 1
@Suppress("UNUSED_VARIABLE")
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// b is removed, d is added
data class CC (val a: Int, val c: String, val d: String) {
// ensure none of the original parameters align with the initial
// construction order
@Suppress("UNUSED")
@CordaSerializerConstructor(1)
constructor (c: String, a: Int) : this (a, c, "wibble")
}
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (A, deserializedCC.a)
assertEquals (C, deserializedCC.c)
assertEquals ("wibble", deserializedCC.d)
}
@Test
fun multiVersion() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3")
@Suppress("UNUSED_VARIABLE")
val f = File(path1.toURI())
val a = 100
val b = 200
val c = 300
val d = 400
// Original version of the class as it was serialised
//
// Version 1:
// data class C (val a: Int, val b: Int)
// Version 2 - add param c
// data class C (val c: Int, val b: Int, val a: Int)
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, a))
// f.writeBytes(scc.bytes)
// println ("Path = $path1")
@Suppress("UNUSED")
data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
@CordaSerializerConstructor(1)
constructor (b: Int, a: Int) : this (-1, -1, b, a, -1)
@CordaSerializerConstructor(2)
constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1)
@CordaSerializerConstructor(3)
constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d)
}
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
assertEquals(a, db1.a)
assertEquals(b, db1.b)
assertEquals(-1, db1.c)
assertEquals(-1, db1.d)
assertEquals(-1, db1.e)
val sb2 = File(path2.toURI()).readBytes()
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
assertEquals(a, db2.a)
assertEquals(b, db2.b)
assertEquals(c, db2.c)
assertEquals(-1, db2.d)
assertEquals(-1, db2.e)
val sb3 = File(path3.toURI()).readBytes()
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
assertEquals(a, db3.a)
assertEquals(b, db3.b)
assertEquals(c, db3.c)
assertEquals(d, db3.d)
assertEquals(-1, db3.e)
}
@Test @Test
fun changeSubType() { fun changeSubType() {
val sf = testDefaultFactory() val sf = testDefaultFactory()
@ -215,4 +380,75 @@ class EvolvabilityTests {
assertEquals (ia, outer.b.a) assertEquals (ia, outer.b.a)
assertEquals (null, outer.b.b) assertEquals (null, outer.b.b)
} }
@Test
fun multiVersionWithRemoval() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3")
@Suppress("UNUSED_VARIABLE")
val a = 100
val b = 200
val c = 300
val d = 400
val e = 500
val f = 600
// Original version of the class as it was serialised
//
// Version 1:
// data class C (val a: Int, val b: Int, val c: Int)
// Version 2 - add param c
// data class C (val b: Int, val c: Int, val d: Int, val e: Int)
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
@Suppress("UNUSED")
data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
@CordaSerializerConstructor(1)
constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1)
@CordaSerializerConstructor(2)
constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1)
@CordaSerializerConstructor(3)
constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1)
@CordaSerializerConstructor(4)
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1)
}
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
assertEquals(b, db1.b)
assertEquals(c, db1.c)
assertEquals(-1, db1.d) // must not be set by calling constructor 2 by mistake
assertEquals(-1, db1.e)
assertEquals(-1, db1.f)
assertEquals(-1, db1.g)
val sb2 = File(path2.toURI()).readBytes()
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
assertEquals(b, db2.b)
assertEquals(c, db2.c)
assertEquals(d, db2.d)
assertEquals(e, db2.e)
assertEquals(-1, db2.f)
assertEquals(-1, db1.g)
val sb3 = File(path3.toURI()).readBytes()
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
assertEquals(b, db3.b)
assertEquals(c, db3.c)
assertEquals(d, db3.d)
assertEquals(e, db3.e)
assertEquals(f, db3.f)
assertEquals(-1, db3.g)
}
} }