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

feat: JSON schema evolution support #1760

Merged

Conversation

aludwiko
Copy link
Contributor

@aludwiko aludwiko commented Aug 8, 2023

reference #1332

@aludwiko aludwiko linked an issue Aug 8, 2023 that may be closed by this pull request
@@ -126,7 +126,7 @@ lazy val javaSdkProtobufTestKit = project

lazy val javaSdkSpring = project
.in(file("sdk/java-sdk-spring"))
.dependsOn(javaSdkProtobuf)
.dependsOn(javaSdkProtobuf % "compile->compile;test->test")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reusing some test classes.

//TODO verify if this could be replaced by sth smarter/safer
reversedCache.compute(
addToReversedCache(clz, typeName)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While updating typeHints map, I'm also updating another map reversedTypeHints, this feels wrong, but I haven't figured out a better option for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I recall correctly, we register the types when we register the components. This is supposed to be done by a single thread running over all components and finding out the types. In that sense, it's safe to update the main map and then the reversed one.

For good measure, we could synchronize this method to make sure we are safe. Just in case something changes one day.

@github-actions github-actions bot added the Documentation Improvements or additions to the documentation label Aug 9, 2023
@aludwiko aludwiko force-pushed the 1332-java-code-first-sdk-support-for-akkas-jackson-migration branch from f99a007 to 634a3b3 Compare August 9, 2023 09:00
@github-actions github-actions bot removed the Documentation Improvements or additions to the documentation label Aug 9, 2023
* @return The decoded object
* @throws IllegalArgumentException if the given value cannot be decoded to a T
*/
public static <T> T decodeJson(Class<T> valueClass, Any any, Optional<? extends JacksonMigration> jacksonMigration) {
Copy link
Contributor Author

@aludwiko aludwiko Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A jacksonMigration additional param to pass the migration from the upper level, otherwise I would need to move the @ Migration annotation here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the signature with Optional param internal and just have one overload with migration and one without, for Java?

Copy link
Contributor Author

@aludwiko aludwiko Aug 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, but then all the invocation would look like:

        extractMigration(typeClass)
          .map(JsonSupport.decodeJson(typeClass, any, _))
          .getOrElse(JsonSupport.decodeJson(typeClass, any))

which is also smelly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some serious doubts about this signature. I want to show serialization test examples for the user. From the test perspective, I need to pass the migration explicitly:

JsonSupport.decodeJson(AddressChanged.class, serialized, Optional.of(new AddressChangedMigration()))

the test can be green, but the in the actual implementation the linking @Migration(AddressChangedMigration.class) annotation might be missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main question is if we want to support migration for JSON in Proto SDK, like here: https://docs.kalix.io/java-protobuf/actions-publishing-subscribing.html#_json If that's the case then I would move not only @ Migration annotation but also @ TypeName, which is even more realistic use case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought user API for proto would skip the reflection-migration-lookup and be that the user supplies migrations manually, for that it just seems messy that they'd need to deal with optional, but for the reflective may-have-migration use I understand why it needs to be optional.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thought: if we support annotation-detection of migration also for proto, can we just hide it inside the JsonSupport instead and the user doesn't have to pass anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I thought user API for proto would skip the reflection-migration-lookup" - in some sense, it will, because the user will just use the public static <T> T decodeJson(Class<T> valueClass, Any any) { method. Although nothing will prevent him to use the signature with migration.

"can we just hide it inside the" - exactly that's my point one method without any extra params. The "only" drawback is that some code-first annotations will be from the java-sdk-spring and some (@Migration, @TypeName ?) from the java-sdk-protobuf.

@aludwiko aludwiko marked this pull request as ready for review August 11, 2023 10:05
@aludwiko
Copy link
Contributor Author

PR is ready for the first review round.

Copy link
Contributor

@johanandren johanandren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did an initial skim-review, looking good

* @return The decoded object
* @throws IllegalArgumentException if the given value cannot be decoded to a T
*/
public static <T> T decodeJson(Class<T> valueClass, Any any, Optional<? extends JacksonMigration> jacksonMigration) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the signature with Optional param internal and just have one overload with migration and one without, for Java?

Copy link
Contributor

@patriknw patriknw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good, recognising much from Akka (which is a good thing)

@@ -0,0 +1,68 @@
/*
* Copyright 2021 Lightbend Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: bumping the year

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's auto-generated by the plugin, I could bump the configuration and update all files, but in a separate PR

public Optional<String> optionalStringValue;

@JsonCreator
public DummyClassRenamed(@JsonProperty("stringValue") String stringValue, @JsonProperty("intValue") int intValue, @JsonProperty("optionalStringValue") Optional<String> optionalStringValue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these JsonProperty needed if you have javacOptions += "-parameters"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, removed

@aludwiko
Copy link
Contributor Author

I will create a separate PR with the documentation.

Copy link
Contributor

@johanandren johanandren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I think the protobuf side usage can be revisited in the future and is good enough for now.

@aludwiko aludwiko merged commit 00297b0 into main Aug 21, 2023
65 checks passed
@aludwiko aludwiko deleted the 1332-java-code-first-sdk-support-for-akkas-jackson-migration branch August 21, 2023 12:04
Copy link
Member

@octonato octonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late for the party, but I think there are a few things that we should change before releasing it. See my comments.

* @return The decoded object
* @throws IllegalArgumentException if the given value cannot be decoded to a T
*/
public static <T> T decodeJson(Class<T> valueClass, Any any, Optional<? extends JsonMigration> jacksonMigration) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arriving late for the party, but I think we should make this one private. No need to augment the API surface with a method receiving Optional<JsonMigration>.

Each time we call this method, we are calling MigrationExtractor.extractMigration and passing the same valueClass we are receiving as the first parameter. So we can simply remove this method and to this call internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a problematic situation. Initially, this method was private, but to make it private I would need to move the @Migration to java-sdk-protobuf.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have the Migration annotation in the java-sdk-protobuf package, no?

Or do we plan to use this method in other contexts?

The decodeJson method is also used when consuming json from a topic, but in such case, I doubt that it will be useful to pass a migration class as the type URL won't have the version number attached to it.

Comment on lines +262 to +264
.params(777));

Thread.sleep(60000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a leftover? Better to wrap it with await and assert the return. Or get the state and assert it is 777

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just ignore it, I removed it in some other PRs.

Comment on lines 272 to +275
String entityId = "hello1";
execute(componentClient.forEventSourcedEntity(entityId)
.call(CounterEntity::increase)
.params(42));
.call(CounterEntity::increase)
.params(42));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange that this is working previous method increased hello1 to 777 and here we are using the same entity id. What am I missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably the order of tests is different than the order in the class

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that has the potential to introduce flakiness. Better to use a different entityId.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I don't see how that could even work. If the ordering is different, it should also fail. One of the tests must fail, not clear why both are passing. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because one of them is not checking anything, I was testing sth and I forgot to delete it. It is fixed on the main branch.

//TODO verify if this could be replaced by sth smarter/safer
reversedCache.compute(
addToReversedCache(clz, typeName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I recall correctly, we register the types when we register the components. This is supposed to be done by a single thread running over all components and finding out the types. In that sense, it's safe to update the main map and then the reversed one.

For good measure, we could synchronize this method to make sure we are safe. Just in case something changes one day.

"decode with new schema version" in {
val encoded = messageCodec.encodeJava(SimpleClass("abc", 10))
val decoded =
JsonSupport.decodeJson(classOf[SimpleClassUpdated], encoded, Optional.of(new SimpleClassUpdatedMigration))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we are adding this extra method receiving Optional<JsonMigration> because we are using classes that belong to the Protobuf package and we don't have access to MigrationExtractor.extractMigration nor the Migration annotation.

IMO, this is another reason to remove this method. We should not increase the public API just because we want to reuse some classes.

@octonato
Copy link
Member

One more thing...

I see comments about Protobuf migration. If we plan to do it, we should build another abstraction.

The class name is JsonMigration and it depends on Jackson classes (JsonNode). Not something that we want or can reuse for Protobuf payloads.

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

Successfully merging this pull request may close these issues.

Java code-first SDK: support for Akka's Jackson migration
4 participants