-
Notifications
You must be signed in to change notification settings - Fork 210
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
Fix issue where custom JSON serializer generates Base64 encoded data in state store #539
base: master
Are you sure you want to change the base?
Changes from 3 commits
536ba0d
4f87869
301587d
199673a
5155e49
0886ff2
db1f822
d73e06b
8c08b82
7153b41
0e71cf5
278c7ff
bea49e8
c735f66
9dc08d4
2d2e162
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -9,7 +9,6 @@ | |||
import io.dapr.actors.ActorId; | ||||
import io.dapr.config.Properties; | ||||
import io.dapr.serializer.DaprObjectSerializer; | ||||
import io.dapr.serializer.DefaultObjectSerializer; | ||||
import io.dapr.utils.TypeRef; | ||||
import reactor.core.publisher.Mono; | ||||
|
||||
|
@@ -27,6 +26,11 @@ class DaprStateAsyncProvider { | |||
*/ | ||||
private static final Charset CHARSET = Properties.STRING_CHARSET.get(); | ||||
|
||||
/** | ||||
* Marker to identify Json serializers. | ||||
*/ | ||||
public static final String JSON_CONTENT_TYPE = "application/json"; | ||||
|
||||
/** | ||||
* Handles special serialization cases. | ||||
*/ | ||||
|
@@ -45,7 +49,7 @@ class DaprStateAsyncProvider { | |||
/** | ||||
* Flag determining if state serializer is the default serializer instead of user provided. | ||||
*/ | ||||
private final boolean isStateSerializerDefault; | ||||
private final boolean isStateSerializerJson; | ||||
|
||||
/** | ||||
* Instantiates a new Actor's state provider. | ||||
|
@@ -56,7 +60,7 @@ class DaprStateAsyncProvider { | |||
DaprStateAsyncProvider(DaprClient daprClient, DaprObjectSerializer stateSerializer) { | ||||
this.daprClient = daprClient; | ||||
this.stateSerializer = stateSerializer; | ||||
this.isStateSerializerDefault = stateSerializer.getClass() == DefaultObjectSerializer.class; | ||||
this.isStateSerializerJson = JSON_CONTENT_TYPE.equals(stateSerializer.getContentType()); | ||||
} | ||||
|
||||
<T> Mono<T> load(String actorType, ActorId actorId, String stateName, TypeRef<T> type) { | ||||
|
@@ -69,7 +73,7 @@ <T> Mono<T> load(String actorType, ActorId actorId, String stateName, TypeRef<T> | |||
} | ||||
|
||||
T response = this.stateSerializer.deserialize(s, type); | ||||
if (this.isStateSerializerDefault && (response instanceof byte[])) { | ||||
if (this.isStateSerializerJson && (response instanceof byte[])) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing this logic will make this not backwards compatible - it means that values saving with the current version of the code will not be able to be read from a newer version. After thinking about this same problem in another PR, I think this should be done by a configuration. When registering the ActorType, a config can be set to determine is the data is written as Text or binary (Base64). The default behavior is Base64. This way, existing code does not break. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi! I understood your point. But we have a unit test that checks that binary processing is OK. See https://github.com/dapr/java-sdk/blob/master/sdk-actors/src/test/java/io/dapr/actors/runtime/DaprStateAsyncProviderTest.java#L166 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @abogdanov37 Adding a new method to the interface would also be a breaking change since apps would need to implement that new interface method after upgrading. I thought about bringing this in as a configuration per Actor Type, which means that it would arrive in this class as a new argument in the constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @artursouza since Java 8 we have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @abogdanov37 There are 2 things here:
If not, I plan to address this in the next milestone. I am not 100% satisfied with our current serializer approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi! Thanks for example. Sorry for my bad testing. I check only for objects but not for primitives ((( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found my place in my code where I convert Base64. I can reproduce this behavior in tests. Take a time to update code to avoid breaking changes. Thank you @artursouza! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @abogdanov37 You might want to enable the new behavior via a System property, this way, apps can opt in when they know it will not break them (new app, for example). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
this solution works for all types exclude String. Strings pass through as is (base64 encoded) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @artursouza I push another solution based on "fair" conversion.
I add code to transform Base64 from store to origin byte array which can be processed. |
||||
if (s.length == 0) { | ||||
return Mono.empty(); | ||||
} | ||||
|
@@ -138,7 +142,7 @@ Mono<Void> apply(String actorType, ActorId actorId, ActorStateChange... stateCha | |||
try { | ||||
byte[] data = this.stateSerializer.serialize(stateChange.getValue()); | ||||
if (data != null) { | ||||
if (this.isStateSerializerDefault && !(stateChange.getValue() instanceof byte[])) { | ||||
if (this.isStateSerializerJson && !(stateChange.getValue() instanceof byte[])) { | ||||
// DefaultObjectSerializer is a JSON serializer, so we just pass it on. | ||||
value = new String(data, CHARSET); | ||||
} else { | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
import org.junit.Test; | ||
import reactor.core.publisher.Mono; | ||
|
||
import java.io.IOException; | ||
import java.util.Arrays; | ||
import java.util.Objects; | ||
|
||
|
@@ -27,8 +28,6 @@ public class DaprStateAsyncProviderTest { | |
|
||
private static final DaprObjectSerializer SERIALIZER = new DefaultObjectSerializer(); | ||
|
||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
||
private static final double EPSILON = 1e-10; | ||
|
||
/** | ||
|
@@ -74,6 +73,28 @@ public int hashCode() { | |
|
||
} | ||
|
||
class CustomJsonSerializer implements DaprObjectSerializer{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a space before |
||
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
||
@Override | ||
public byte[] serialize(Object o) throws IOException { | ||
return OBJECT_MAPPER.writeValueAsBytes(o); | ||
} | ||
|
||
@Override | ||
public <T> T deserialize(byte[] data, TypeRef<T> type) throws IOException { | ||
if (data.length == 0) { | ||
return null; | ||
} | ||
return OBJECT_MAPPER.readValue(data, OBJECT_MAPPER.constructType(type.getType())); | ||
} | ||
|
||
@Override | ||
public String getContentType() { | ||
return "application/json"; | ||
} | ||
} | ||
|
||
@Test | ||
public void happyCaseApply() { | ||
DaprClient daprClient = mock(DaprClient.class); | ||
|
@@ -195,6 +216,32 @@ public void happyCaseLoad() throws Exception { | |
provider.load("MyActor", new ActorId("123"), "bytes", TypeRef.get(byte[].class)).block()); | ||
Assert.assertNull( | ||
provider.load("MyActor", new ActorId("123"), "emptyBytes", TypeRef.get(byte[].class)).block()); | ||
|
||
DaprStateAsyncProvider providerWithCustomJsonSerializer = new DaprStateAsyncProvider(daprClient, new CustomJsonSerializer()); | ||
|
||
Assert.assertEquals("Jon Doe", | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "name", TypeRef.STRING).block()); | ||
Assert.assertEquals(98021, | ||
(int) providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "zipcode", TypeRef.INT).block()); | ||
Assert.assertEquals(98, | ||
(int) providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "goals", TypeRef.INT).block()); | ||
Assert.assertEquals(98, | ||
(int) providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "goals", TypeRef.INT).block()); | ||
Assert.assertEquals(46.55, | ||
(double) providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "balance", TypeRef.DOUBLE).block(), | ||
EPSILON); | ||
Assert.assertEquals(true, | ||
(boolean) providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "active", TypeRef.BOOLEAN).block()); | ||
Assert.assertEquals(new Customer().setId(1000).setName("Roxane"), | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "customer", TypeRef.get(Customer.class)).block()); | ||
Assert.assertNotEquals(new Customer().setId(1000).setName("Roxane"), | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "anotherCustomer", TypeRef.get(Customer.class)).block()); | ||
Assert.assertNull( | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "nullCustomer", TypeRef.get(Customer.class)).block()); | ||
Assert.assertArrayEquals("A".getBytes(), | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "bytes", TypeRef.get(byte[].class)).block()); | ||
Assert.assertNull( | ||
providerWithCustomJsonSerializer.load("MyActor", new ActorId("123"), "emptyBytes", TypeRef.get(byte[].class)).block()); | ||
} | ||
|
||
@Test | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Static final variables should be capitalized: ERROR_MSG